QPID-8484: [Broker-J] Reimplementation of the limit number of connections per user

This closes #76
diff --git a/apache-qpid-broker-j/src/main/assembly/dependency-verification/DEPENDENCIES_REFERENCE b/apache-qpid-broker-j/src/main/assembly/dependency-verification/DEPENDENCIES_REFERENCE
index 59f6b5b..137a264 100644
--- a/apache-qpid-broker-j/src/main/assembly/dependency-verification/DEPENDENCIES_REFERENCE
+++ b/apache-qpid-broker-j/src/main/assembly/dependency-verification/DEPENDENCIES_REFERENCE
@@ -85,6 +85,8 @@
     License: Apache License, Version 2.0  (https://www.apache.org/licenses/LICENSE-2.0.txt)
   - Apache Qpid Broker-J Access Control Plug-in (http://qpid.apache.org/components/broker-plugins/qpid-broker-plugins-access-control) org.apache.qpid:qpid-broker-plugins-access-control:jar
     License: Apache License, Version 2.0  (https://www.apache.org/licenses/LICENSE-2.0.txt)
+  - Apache Qpid Broker-J Connection Limits Plug-in (http://qpid.apache.org/components/broker-plugins/qpid-broker-plugins-connection-limits) org.apache.qpid:qpid-broker-plugins-connection-limits:jar
+    License: Apache License, Version 2.0  (https://www.apache.org/licenses/LICENSE-2.0.txt)
   - Apache Qpid Broker-J AMQP 0-10 Protocol Plug-in (http://qpid.apache.org/components/broker-plugins/qpid-broker-plugins-amqp-0-10-protocol) org.apache.qpid:qpid-broker-plugins-amqp-0-10-protocol:jar
     License: Apache License, Version 2.0  (https://www.apache.org/licenses/LICENSE-2.0.txt)
   - Apache Qpid Broker-J AMQP 0-8 Protocol Plug-in (http://qpid.apache.org/components/broker-plugins/qpid-broker-plugins-amqp-0-8-protocol) org.apache.qpid:qpid-broker-plugins-amqp-0-8-protocol:jar
diff --git a/broker-core/src/main/java/org/apache/qpid/server/logging/messages/ResourceLimitMessages.java b/broker-core/src/main/java/org/apache/qpid/server/logging/messages/ResourceLimitMessages.java
new file mode 100644
index 0000000..743db3e
--- /dev/null
+++ b/broker-core/src/main/java/org/apache/qpid/server/logging/messages/ResourceLimitMessages.java
@@ -0,0 +1,203 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ *
+ *
+ */
+package org.apache.qpid.server.logging.messages;
+
+import static org.apache.qpid.server.logging.AbstractMessageLogger.DEFAULT_LOG_HIERARCHY_PREFIX;
+
+import java.text.MessageFormat;
+import java.util.Locale;
+import java.util.ResourceBundle;
+
+import org.slf4j.LoggerFactory;
+
+import org.apache.qpid.server.logging.LogMessage;
+
+/**
+ * DO NOT EDIT DIRECTLY, THIS FILE WAS GENERATED.
+ *
+ * Generated using GenerateLogMessages and LogMessages.vm
+ * This file is based on the content of ResourceLimit_logmessages.properties
+ *
+ * To regenerate, use Maven lifecycle generates-sources with -Dgenerate=true
+ */
+public class ResourceLimitMessages
+{
+    private static ResourceBundle _messages;
+    private static Locale _currentLocale;
+
+    static
+    {
+        Locale locale = Locale.US;
+        String localeSetting = System.getProperty("qpid.broker_locale");
+        if (localeSetting != null)
+        {
+            String[] localeParts = localeSetting.split("_");
+            String language = (localeParts.length > 0 ? localeParts[0] : "");
+            String country = (localeParts.length > 1 ? localeParts[1] : "");
+            String variant = "";
+            if (localeParts.length > 2)
+            {
+                variant = localeSetting.substring(language.length() + 1 + country.length() + 1);
+            }
+            locale = new Locale(language, country, variant);
+        }
+        _currentLocale = locale;
+    }
+
+    public static final String RESOURCELIMIT_LOG_HIERARCHY = DEFAULT_LOG_HIERARCHY_PREFIX + "resourcelimit";
+    public static final String ACCEPTED_LOG_HIERARCHY = DEFAULT_LOG_HIERARCHY_PREFIX + "resourcelimit.accepted";
+    public static final String REJECTED_LOG_HIERARCHY = DEFAULT_LOG_HIERARCHY_PREFIX + "resourcelimit.rejected";
+
+    static
+    {
+        LoggerFactory.getLogger(RESOURCELIMIT_LOG_HIERARCHY);
+        LoggerFactory.getLogger(ACCEPTED_LOG_HIERARCHY);
+        LoggerFactory.getLogger(REJECTED_LOG_HIERARCHY);
+
+        _messages = ResourceBundle.getBundle("org.apache.qpid.server.logging.messages.ResourceLimit_logmessages", _currentLocale);
+    }
+
+    /**
+     * Log a ResourceLimit message of the Format:
+     * <pre>RL-1001 : Accepted : {0} {1} by {2} : {3}</pre>
+     * Optional values are contained in [square brackets] and are numbered
+     * sequentially in the method call.
+     *
+     */
+    public static LogMessage ACCEPTED(String param1, String param2, String param3, String param4)
+    {
+        String rawMessage = _messages.getString("ACCEPTED");
+
+        final Object[] messageArguments = {param1, param2, param3, param4};
+        // Create a new MessageFormat to ensure thread safety.
+        // Sharing a MessageFormat and using applyPattern is not thread safe
+        MessageFormat formatter = new MessageFormat(rawMessage, _currentLocale);
+
+        final String message = formatter.format(messageArguments);
+
+        return new LogMessage()
+        {
+            @Override
+            public String toString()
+            {
+                return message;
+            }
+
+            @Override
+            public String getLogHierarchy()
+            {
+                return ACCEPTED_LOG_HIERARCHY;
+            }
+
+            @Override
+            public boolean equals(final Object o)
+            {
+                if (this == o)
+                {
+                    return true;
+                }
+                if (o == null || getClass() != o.getClass())
+                {
+                    return false;
+                }
+
+                final LogMessage that = (LogMessage) o;
+
+                return getLogHierarchy().equals(that.getLogHierarchy()) && toString().equals(that.toString());
+
+            }
+
+            @Override
+            public int hashCode()
+            {
+                int result = toString().hashCode();
+                result = 31 * result + getLogHierarchy().hashCode();
+                return result;
+            }
+        };
+    }
+
+    /**
+     * Log a ResourceLimit message of the Format:
+     * <pre>RL-1002 : Rejected : {0} {1} by {2} : {3}</pre>
+     * Optional values are contained in [square brackets] and are numbered
+     * sequentially in the method call.
+     *
+     */
+    public static LogMessage REJECTED(String param1, String param2, String param3, String param4)
+    {
+        String rawMessage = _messages.getString("REJECTED");
+
+        final Object[] messageArguments = {param1, param2, param3, param4};
+        // Create a new MessageFormat to ensure thread safety.
+        // Sharing a MessageFormat and using applyPattern is not thread safe
+        MessageFormat formatter = new MessageFormat(rawMessage, _currentLocale);
+
+        final String message = formatter.format(messageArguments);
+
+        return new LogMessage()
+        {
+            @Override
+            public String toString()
+            {
+                return message;
+            }
+
+            @Override
+            public String getLogHierarchy()
+            {
+                return REJECTED_LOG_HIERARCHY;
+            }
+
+            @Override
+            public boolean equals(final Object o)
+            {
+                if (this == o)
+                {
+                    return true;
+                }
+                if (o == null || getClass() != o.getClass())
+                {
+                    return false;
+                }
+
+                final LogMessage that = (LogMessage) o;
+
+                return getLogHierarchy().equals(that.getLogHierarchy()) && toString().equals(that.toString());
+
+            }
+
+            @Override
+            public int hashCode()
+            {
+                int result = toString().hashCode();
+                result = 31 * result + getLogHierarchy().hashCode();
+                return result;
+            }
+        };
+    }
+
+
+    private ResourceLimitMessages()
+    {
+    }
+
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/logging/messages/ResourceLimit_logmessages.properties b/broker-core/src/main/java/org/apache/qpid/server/logging/messages/ResourceLimit_logmessages.properties
new file mode 100644
index 0000000..a78feed
--- /dev/null
+++ b/broker-core/src/main/java/org/apache/qpid/server/logging/messages/ResourceLimit_logmessages.properties
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+# User Resource Limit logging message i18n strings.
+ACCEPTED = RL-1001 : Accepted : {0} {1} by {2} : {3}
+REJECTED = RL-1002 : Rejected : {0} {1} by {2} : {3}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java b/broker-core/src/main/java/org/apache/qpid/server/model/BrokerConnectionLimitProvider.java
similarity index 75%
copy from broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
copy to broker-core/src/main/java/org/apache/qpid/server/model/BrokerConnectionLimitProvider.java
index 661b89b..9b4ad9b 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/model/BrokerConnectionLimitProvider.java
@@ -16,12 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.qpid.server.model;
 
-package org.apache.qpid.server.virtualhost;
+import org.apache.qpid.server.security.limit.ConnectionLimitProvider;
 
-public interface ConnectionPrincipalStatistics
+@ManagedObject
+public interface BrokerConnectionLimitProvider<X extends BrokerConnectionLimitProvider<X>>
+        extends ConnectionLimitProvider<X>
 {
-    int getConnectionCount();
-
-    int getConnectionFrequency();
 }
diff --git a/broker-core/src/main/java/org/apache/qpid/server/model/BrokerModel.java b/broker-core/src/main/java/org/apache/qpid/server/model/BrokerModel.java
index 78176f2..8953297 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/model/BrokerModel.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/model/BrokerModel.java
@@ -113,6 +113,7 @@
         addRelationship(Broker.class, AuthenticationProvider.class);
         addRelationship(Broker.class, GroupProvider.class);
         addRelationship(Broker.class, Plugin.class);
+        addRelationship(Broker.class, BrokerConnectionLimitProvider.class);
 
         addRelationship(BrokerLogger.class, BrokerLogInclusionRule.class);
 
@@ -123,6 +124,7 @@
         addRelationship(VirtualHost.class, VirtualHostAccessControlProvider.class);
         addRelationship(VirtualHost.class, Exchange.class);
         addRelationship(VirtualHost.class, Queue.class);
+        addRelationship(VirtualHost.class, VirtualHostConnectionLimitProvider.class);
 
         addRelationship(VirtualHostLogger.class, VirtualHostLogInclusionRule.class);
 
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java b/broker-core/src/main/java/org/apache/qpid/server/model/VirtualHostConnectionLimitProvider.java
similarity index 74%
copy from broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
copy to broker-core/src/main/java/org/apache/qpid/server/model/VirtualHostConnectionLimitProvider.java
index 661b89b..311838f 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/model/VirtualHostConnectionLimitProvider.java
@@ -16,12 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.qpid.server.model;
 
-package org.apache.qpid.server.virtualhost;
+import org.apache.qpid.server.security.limit.ConnectionLimitProvider;
 
-public interface ConnectionPrincipalStatistics
+@ManagedObject
+public interface VirtualHostConnectionLimitProvider<X extends VirtualHostConnectionLimitProvider<X>>
+        extends ConnectionLimitProvider<X>
 {
-    int getConnectionCount();
-
-    int getConnectionFrequency();
 }
diff --git a/broker-core/src/main/java/org/apache/qpid/server/security/limit/CachedConnectionLimiterImpl.java b/broker-core/src/main/java/org/apache/qpid/server/security/limit/CachedConnectionLimiterImpl.java
new file mode 100644
index 0000000..08fa45c
--- /dev/null
+++ b/broker-core/src/main/java/org/apache/qpid/server/security/limit/CachedConnectionLimiterImpl.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.security.limit;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.qpid.server.security.limit.ConnectionLimiter.CachedLimiter;
+import org.apache.qpid.server.transport.AMQPConnection;
+
+public class CachedConnectionLimiterImpl implements CachedLimiter
+{
+    private final Map<AMQPConnection<?>, ConnectionSlot> _cache = new ConcurrentHashMap<>();
+
+    private final AtomicReference<ConnectionLimiter> _limiter;
+
+    public CachedConnectionLimiterImpl(ConnectionLimiter limiter)
+    {
+        super();
+        _limiter = new AtomicReference<>(Objects.requireNonNull(limiter));
+    }
+
+    @Override
+    public ConnectionSlot register(AMQPConnection<?> connection)
+    {
+        return _cache.computeIfAbsent(connection, this::newConnectionSlot);
+    }
+
+    @Override
+    public boolean deregister(AMQPConnection<?> connection)
+    {
+        final ConnectionSlot connectionSlot = _cache.get(connection);
+        if (connectionSlot != null)
+        {
+            connectionSlot.free();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public ConnectionLimiter append(ConnectionLimiter limiter)
+    {
+        return _limiter.get().append(limiter);
+    }
+
+    protected void swapLimiter(ConnectionLimiter limiter)
+    {
+        _limiter.set(Objects.requireNonNull(limiter));
+    }
+
+    private ConnectionSlot newConnectionSlot(AMQPConnection<?> connection)
+    {
+        return new ConnectionSlotImpl(connection, _limiter.get().register(connection));
+    }
+
+    private final class ConnectionSlotImpl implements ConnectionSlot
+    {
+        private final AMQPConnection<?> _connection;
+
+        private final ConnectionSlot _slot;
+
+        ConnectionSlotImpl(AMQPConnection<?> connection, ConnectionSlot slot)
+        {
+            _connection = Objects.requireNonNull(connection);
+            _slot = Objects.requireNonNull(slot);
+        }
+
+        @Override
+        public void free()
+        {
+            if (_cache.remove(_connection, this))
+            {
+                _slot.free();
+            }
+        }
+    }
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java b/broker-core/src/main/java/org/apache/qpid/server/security/limit/ConnectionLimitException.java
similarity index 80%
copy from broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
copy to broker-core/src/main/java/org/apache/qpid/server/security/limit/ConnectionLimitException.java
index 661b89b..eafb6ab 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/security/limit/ConnectionLimitException.java
@@ -16,12 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.qpid.server.security.limit;
 
-package org.apache.qpid.server.virtualhost;
-
-public interface ConnectionPrincipalStatistics
+public class ConnectionLimitException extends RuntimeException
 {
-    int getConnectionCount();
-
-    int getConnectionFrequency();
+    public ConnectionLimitException(String s)
+    {
+        super(s);
+    }
 }
diff --git a/broker-core/src/main/java/org/apache/qpid/server/security/limit/ConnectionLimitProvider.java b/broker-core/src/main/java/org/apache/qpid/server/security/limit/ConnectionLimitProvider.java
new file mode 100644
index 0000000..5d49375
--- /dev/null
+++ b/broker-core/src/main/java/org/apache/qpid/server/security/limit/ConnectionLimitProvider.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.security.limit;
+
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.ManagedContextDefault;
+import org.apache.qpid.server.model.ManagedObject;
+
+@ManagedObject(category = false)
+public interface ConnectionLimitProvider<X extends ConfiguredObject<X>> extends ConfiguredObject<X>
+{
+    String CONNECTION_FREQUENCY_PERIOD = "qpid.broker.connectionLimiter.frequencyPeriodInMillis";
+
+    @ManagedContextDefault(name = CONNECTION_FREQUENCY_PERIOD,
+            description = "Interval (in milliseconds) to evaluate connection frequency")
+    @SuppressWarnings("unused")
+    long DEFAULT_CONNECTION_FREQUENCY_PERIOD = 60L * 1000L;
+
+    ConnectionLimiter getConnectionLimiter();
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/security/limit/ConnectionLimiter.java b/broker-core/src/main/java/org/apache/qpid/server/security/limit/ConnectionLimiter.java
new file mode 100644
index 0000000..3bab375
--- /dev/null
+++ b/broker-core/src/main/java/org/apache/qpid/server/security/limit/ConnectionLimiter.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.security.limit;
+
+import java.util.Objects;
+
+import org.apache.qpid.server.security.limit.ConnectionSlot.FreeSlot;
+import org.apache.qpid.server.transport.AMQPConnection;
+
+public interface ConnectionLimiter
+{
+    ConnectionSlot register(AMQPConnection<?> connection);
+
+    ConnectionLimiter append(ConnectionLimiter limiter);
+
+    static CachedLimiter noLimits()
+    {
+        return NoLimits.INSTANCE;
+    }
+
+    static CachedLimiter blockEveryone()
+    {
+        return BlockEveryone.INSTANCE;
+    }
+
+    interface CachedLimiter extends ConnectionLimiter
+    {
+        boolean deregister(AMQPConnection<?> connection);
+    }
+
+    final class NoLimits implements CachedLimiter
+    {
+        static CachedLimiter INSTANCE = new NoLimits();
+
+        private NoLimits()
+        {
+            super();
+        }
+
+        @Override
+        public ConnectionSlot register(AMQPConnection<?> connection)
+        {
+            return FreeSlot.INSTANCE;
+        }
+
+        @Override
+        public boolean deregister(AMQPConnection<?> connection)
+        {
+            return true;
+        }
+
+        @Override
+        public ConnectionLimiter append(ConnectionLimiter limiter)
+        {
+            return Objects.requireNonNull(limiter);
+        }
+    }
+
+    final class BlockEveryone implements CachedLimiter
+    {
+        static CachedLimiter INSTANCE = new BlockEveryone();
+
+        private BlockEveryone()
+        {
+            super();
+        }
+
+        @Override
+        public ConnectionSlot register(AMQPConnection<?> connection)
+        {
+            throw new ConnectionLimitException("Opening any new connection is forbidden");
+        }
+
+        @Override
+        public boolean deregister(AMQPConnection<?> connection)
+        {
+            return false;
+        }
+
+        @Override
+        public ConnectionLimiter append(ConnectionLimiter limiter)
+        {
+            Objects.requireNonNull(limiter);
+            return this;
+        }
+    }
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/security/limit/ConnectionSlot.java b/broker-core/src/main/java/org/apache/qpid/server/security/limit/ConnectionSlot.java
new file mode 100644
index 0000000..e176d53
--- /dev/null
+++ b/broker-core/src/main/java/org/apache/qpid/server/security/limit/ConnectionSlot.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.security.limit;
+
+import java.util.Optional;
+
+@FunctionalInterface
+public interface ConnectionSlot extends Runnable
+{
+    void free();
+
+    @Override
+    default void run()
+    {
+        free();
+    }
+
+    default ConnectionSlot chainTo(final ConnectionSlot secondarySlot)
+    {
+        if (secondarySlot == null || secondarySlot instanceof FreeSlot)
+        {
+            return this;
+        }
+        final ConnectionSlot primarySlot = this;
+        return () ->
+        {
+            try
+            {
+                secondarySlot.free();
+            }
+            finally
+            {
+                primarySlot.free();
+            }
+        };
+    }
+
+    final class FreeSlot implements ConnectionSlot
+    {
+        public static final FreeSlot INSTANCE = new FreeSlot();
+
+        private FreeSlot()
+        {
+            super();
+        }
+
+        @Override
+        public void free()
+        {
+            // Do nothing
+        }
+
+        @Override
+        public ConnectionSlot chainTo(ConnectionSlot secondarySlot)
+        {
+            return Optional.ofNullable(secondarySlot).orElse(this);
+        }
+    }
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java b/broker-core/src/main/java/org/apache/qpid/server/security/limit/package-info.java
similarity index 82%
copy from broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
copy to broker-core/src/main/java/org/apache/qpid/server/security/limit/package-info.java
index 661b89b..9977d85 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/security/limit/package-info.java
@@ -16,12 +16,4 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-package org.apache.qpid.server.virtualhost;
-
-public interface ConnectionPrincipalStatistics
-{
-    int getConnectionCount();
-
-    int getConnectionFrequency();
-}
+package org.apache.qpid.server.security.limit;
diff --git a/broker-core/src/main/java/org/apache/qpid/server/transport/AMQPConnection.java b/broker-core/src/main/java/org/apache/qpid/server/transport/AMQPConnection.java
index ebcd737..97fc81f 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/transport/AMQPConnection.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/transport/AMQPConnection.java
@@ -39,7 +39,6 @@
 import org.apache.qpid.server.txn.ServerTransaction;
 import org.apache.qpid.server.util.Action;
 import org.apache.qpid.server.util.Deletable;
-import org.apache.qpid.server.virtualhost.ConnectionPrincipalStatistics;
 
 public interface AMQPConnection<C extends AMQPConnection<C>>
         extends Connection<C>, Deletable<C>, EventLoggerProvider
@@ -147,11 +146,4 @@
 
     @Override
     AmqpPort<?> getPort();
-
-    void registered(ConnectionPrincipalStatistics connectionPrincipalStatistics);
-
-    int getAuthenticatedPrincipalConnectionCount();
-
-    int getAuthenticatedPrincipalConnectionFrequency();
-
 }
diff --git a/broker-core/src/main/java/org/apache/qpid/server/transport/AbstractAMQPConnection.java b/broker-core/src/main/java/org/apache/qpid/server/transport/AbstractAMQPConnection.java
index 74977c3..e21d1a2 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/transport/AbstractAMQPConnection.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/transport/AbstractAMQPConnection.java
@@ -84,7 +84,6 @@
 import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
 import org.apache.qpid.server.util.FixedKeyMapCreator;
 import org.apache.qpid.server.util.ServerScopedRuntimeException;
-import org.apache.qpid.server.virtualhost.ConnectionPrincipalStatistics;
 import org.apache.qpid.server.virtualhost.QueueManagingVirtualHost;
 
 public abstract class AbstractAMQPConnection<C extends AbstractAMQPConnection<C,T>, T>
@@ -150,7 +149,6 @@
     private long _maxUncommittedInMemorySize;
 
     private final Map<ServerTransaction, Set<Ticker>> _transactionTickers = new ConcurrentHashMap<>();
-    private volatile ConnectionPrincipalStatistics _connectionPrincipalStatistics;
 
     public AbstractAMQPConnection(Broker<?> broker,
                                   ServerNetworkConnection network,
@@ -371,7 +369,7 @@
     @Override
     public void pushScheduler(final NetworkConnectionScheduler networkConnectionScheduler)
     {
-        if(_network instanceof NonBlockingConnection)
+        if (_network instanceof NonBlockingConnection)
         {
             ((NonBlockingConnection) _network).pushScheduler(networkConnectionScheduler);
         }
@@ -380,7 +378,7 @@
     @Override
     public NetworkConnectionScheduler popScheduler()
     {
-        if(_network instanceof NonBlockingConnection)
+        if (_network instanceof NonBlockingConnection)
         {
             return ((NonBlockingConnection) _network).popScheduler();
         }
@@ -1159,32 +1157,6 @@
     }
 
     @Override
-    public void registered(final ConnectionPrincipalStatistics connectionPrincipalStatistics)
-    {
-        _connectionPrincipalStatistics = connectionPrincipalStatistics;
-    }
-
-    @Override
-    public int getAuthenticatedPrincipalConnectionCount()
-    {
-        if (_connectionPrincipalStatistics == null)
-        {
-            return 0;
-        }
-        return _connectionPrincipalStatistics.getConnectionCount();
-    }
-
-    @Override
-    public int getAuthenticatedPrincipalConnectionFrequency()
-    {
-        if (_connectionPrincipalStatistics == null)
-        {
-            return 0;
-        }
-        return _connectionPrincipalStatistics.getConnectionFrequency();
-    }
-
-    @Override
     protected void logCreated(final Map<String, Object> attributes,
                               final Outcome outcome)
     {
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/AbstractVirtualHost.java b/broker-core/src/main/java/org/apache/qpid/server/virtualhost/AbstractVirtualHost.java
index 3b8fbd2..c4c0ff5 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/AbstractVirtualHost.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/virtualhost/AbstractVirtualHost.java
@@ -38,7 +38,6 @@
 import java.security.AccessControlContext;
 import java.security.Principal;
 import java.security.PrivilegedAction;
-import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -149,7 +148,6 @@
 import org.apache.qpid.server.txn.ServerTransaction;
 import org.apache.qpid.server.util.HousekeepingExecutor;
 import org.apache.qpid.server.util.Strings;
-import org.apache.qpid.server.virtualhost.connection.ConnectionPrincipalStatisticsRegistryImpl;
 
 public abstract class AbstractVirtualHost<X extends AbstractVirtualHost<X>> extends AbstractConfiguredObject<X>
         implements QueueManagingVirtualHost<X>
@@ -230,6 +228,7 @@
         }
     }, Result.DEFER);
 
+    private final VirtualHostConnectionLimiter _connectionLimiter;
 
     @ManagedAttributeField
     private boolean _queue_deadLetterQueueEnabled;
@@ -272,7 +271,7 @@
 
     @ManagedAttributeField
     private volatile int _statisticsReportingPeriod;
-    
+
     private boolean _useAsyncRecoverer;
 
     private MessageDestination _defaultDestination;
@@ -285,8 +284,6 @@
     private PreferenceStore _preferenceStore;
     private long _flowToDiskCheckPeriod;
     private volatile boolean _isDiscardGlobalSharedSubscriptionLinksOnDetach;
-    private volatile ConnectionPrincipalStatisticsRegistry _connectionPrincipalStatisticsRegistry;
-    private volatile HouseKeepingTask _statisticsCheckTask;
 
     public AbstractVirtualHost(final Map<String, Object> attributes, VirtualHostNode<?> virtualHostNode)
     {
@@ -319,6 +316,7 @@
         _fileSystemSpaceCheckerJobContext = getSystemTaskControllerContext("FileSystemSpaceChecker["+getName()+"]", _principal);
 
         _fileSystemSpaceChecker = new FileSystemSpaceChecker();
+        _connectionLimiter = new VirtualHostConnectionLimiter(this, _broker);
     }
 
     private void updateAccessControl()
@@ -351,6 +349,21 @@
         }
     }
 
+    private void openConnectionLimiter()
+    {
+        _connectionLimiter.open();
+    }
+
+    private void activateConnectionLimiter()
+    {
+        _connectionLimiter.activate();
+    }
+
+    private void closeConnectionLimiter()
+    {
+        _connectionLimiter.close();
+    }
+
     @Override
     protected void onCreate()
     {
@@ -588,6 +601,7 @@
         closeMessageStore();
         stopPreferenceTaskExecutor();
         closePreferenceStore();
+        closeConnectionLimiter();
         stopLogging(new ArrayList<>(getChildren(VirtualHostLogger.class)));
     }
 
@@ -627,6 +641,7 @@
         _linkRegistry = createLinkRegistry();
 
         createHousekeepingExecutor();
+        openConnectionLimiter();
     }
 
     LinkRegistryModel createLinkRegistry()
@@ -1616,6 +1631,8 @@
 
         stopLogging(_virtualHostLoggersToClose);
         _systemNodeRegistry.close();
+
+        closeConnectionLimiter();
         return Futures.immediateFuture(null);
     }
 
@@ -2444,7 +2461,6 @@
             @Override
             public void run()
             {
-                resetConnectionPrincipalStatisticsRegistry();
                 shutdownHouseKeeping();
                 closeNetworkConnectionScheduler();
                 if (_linkRegistry != null)
@@ -2456,6 +2472,7 @@
                 closePreferenceStore();
                 setState(State.STOPPED);
                 stopLogging(loggers);
+                closeConnectionLimiter();
             }
         });
     }
@@ -2585,6 +2602,7 @@
     public boolean registerConnection(final AMQPConnection<?> connection,
                                       final ConnectionEstablishmentPolicy connectionEstablishmentPolicy)
     {
+        _connectionLimiter.register(connection);
         return doSync(registerConnectionAsync(connection, connectionEstablishmentPolicy));
     }
 
@@ -2600,9 +2618,6 @@
                 {
                     if (connectionEstablishmentPolicy.mayEstablishNewConnection(_connections, connection))
                     {
-                        final ConnectionPrincipalStatistics cps =
-                                _connectionPrincipalStatisticsRegistry.connectionOpened(connection);
-                        connection.registered(cps);
                         _connections.add(connection);
                         _totalConnectionCount.incrementAndGet();
 
@@ -2653,7 +2668,15 @@
     @Override
     public void deregisterConnection(final AMQPConnection<?> connection)
     {
-        doSync(deregisterConnectionAsync(connection));
+        try
+        {
+            _connectionLimiter.deregister(connection);
+        }
+        finally
+        {
+            doSync(deregisterConnectionAsync(connection));
+
+        }
     }
 
     public ListenableFuture<Void> deregisterConnectionAsync(final AMQPConnection<?> connection)
@@ -2665,8 +2688,6 @@
             {
                 connection.popScheduler();
                 _connections.remove(connection);
-                _connectionPrincipalStatisticsRegistry.connectionClosed(connection);
-
                 return Futures.immediateFuture(null);
             }
 
@@ -2709,7 +2730,6 @@
 
         updateAccessControl();
         initialiseStatisticsReporting();
-        initialiseConnectionPrincipalStatisticsRegistry();
 
         MessageStore messageStore = getMessageStore();
         messageStore.openMessageStore(this);
@@ -2739,6 +2759,8 @@
         PreferencesRecoverer preferencesRecoverer = new PreferencesRecoverer(_preferenceTaskExecutor);
         preferencesRecoverer.recoverPreferences(this, records, _preferenceStore);
 
+        activateConnectionLimiter();
+
         if (_createDefaultExchanges)
         {
             return doAfter(createDefaultExchanges(), new Runnable()
@@ -2758,42 +2780,6 @@
         }
     }
 
-    private void initialiseConnectionPrincipalStatisticsRegistry()
-    {
-        final long connectionFrequencyPeriodMillis = getContextValue(Long.class, CONNECTION_FREQUENCY_PERIOD);
-        final Duration connectionFrequencyPeriod = Duration.ofMillis(connectionFrequencyPeriodMillis);
-        final ConnectionPrincipalStatisticsRegistryImpl connectionStatisticsRegistry =
-                new ConnectionPrincipalStatisticsRegistryImpl(() -> connectionFrequencyPeriod);
-        HouseKeepingTask task = null;
-        long taskRunPeriod = connectionFrequencyPeriodMillis / 2;
-        if (taskRunPeriod > 0)
-        {
-            final AccessControlContext context =
-                    getSystemTaskControllerContext("ConnectionPrincipalStatisticsCheck", _principal);
-            task = new ConnectionPrincipalStatisticsCheckingTask(this, context, connectionStatisticsRegistry);
-            scheduleHouseKeepingTask(taskRunPeriod, task);
-        }
-        _statisticsCheckTask = task;
-        _connectionPrincipalStatisticsRegistry = connectionStatisticsRegistry;
-    }
-
-    private void resetConnectionPrincipalStatisticsRegistry()
-    {
-        final HouseKeepingTask previousStatisticsCheckTask = _statisticsCheckTask;
-        if (previousStatisticsCheckTask != null)
-        {
-            previousStatisticsCheckTask.cancel();
-        }
-        _statisticsCheckTask = null;
-        final ConnectionPrincipalStatisticsRegistry connectionPrincipalStatisticsRegistry =
-                _connectionPrincipalStatisticsRegistry;
-        if (connectionPrincipalStatisticsRegistry != null)
-        {
-            connectionPrincipalStatisticsRegistry.reset();
-        }
-        _connectionPrincipalStatisticsRegistry = null;
-    }
-
     private void postCreateDefaultExchangeTasks()
     {
         if(getContextValue(Boolean.class, USE_ASYNC_RECOVERY))
@@ -2947,6 +2933,7 @@
             closePreferenceStore();
             setState(State.ERRORED);
             stopLogging(loggers);
+            closeConnectionLimiter();
         });
     }
 
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatisticsCheckingTask.java b/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatisticsCheckingTask.java
deleted file mode 100644
index 85322bd..0000000
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatisticsCheckingTask.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.qpid.server.virtualhost;
-
-import java.security.AccessControlContext;
-
-public class ConnectionPrincipalStatisticsCheckingTask extends HouseKeepingTask
-{
-    private static final String TASK_NAME = "ConnectionPrincipalStatisticsCheck";
-    private final ConnectionPrincipalStatisticsRegistry _connectionPrincipalStatisticsRegistry;
-
-    public ConnectionPrincipalStatisticsCheckingTask(final QueueManagingVirtualHost virtualHost,
-                                                     final AccessControlContext controlContext,
-                                                     final ConnectionPrincipalStatisticsRegistry connectionPrincipalStatisticsRegistry)
-    {
-        super(TASK_NAME, virtualHost, controlContext);
-        _connectionPrincipalStatisticsRegistry = connectionPrincipalStatisticsRegistry;
-    }
-
-    @Override
-    public void execute()
-    {
-        _connectionPrincipalStatisticsRegistry.reevaluateConnectionStatistics();
-    }
-}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionStatisticsRegistrySettings.java b/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionStatisticsRegistrySettings.java
deleted file mode 100644
index d9611e6..0000000
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionStatisticsRegistrySettings.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.qpid.server.virtualhost;
-
-import java.time.Duration;
-
-public interface ConnectionStatisticsRegistrySettings
-{
-    Duration getConnectionFrequencyPeriod();
-}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/QueueManagingVirtualHost.java b/broker-core/src/main/java/org/apache/qpid/server/virtualhost/QueueManagingVirtualHost.java
index 2022aa4..4cd276a 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/QueueManagingVirtualHost.java
+++ b/broker-core/src/main/java/org/apache/qpid/server/virtualhost/QueueManagingVirtualHost.java
@@ -104,11 +104,6 @@
                                          + " link detaches. This is to avoid leaking links with the Qpid JMS client.")
     boolean DEFAULT_DISCARD_GLOBAL_SHARED_SUBSCRIPTION_LINKS_ON_DETACH = true;
 
-    String CONNECTION_FREQUENCY_PERIOD = "qpid.virtualhost.connectionFrequencyPeriodInMillis";
-    @ManagedContextDefault(name = CONNECTION_FREQUENCY_PERIOD, description = "Interval (in milliseconds) to evaluate connection frequency")
-    @SuppressWarnings("unused")
-    long DEFAULT_CONNECTION_FREQUENCY_PERIOD = 60 * 1000;
-
     @ManagedAttribute( defaultValue = "${" + VIRTUALHOST_STATISTICS_REPORING_PERIOD + "}", description = "Period (in seconds) of the statistic report.")
     int getStatisticsReportingPeriod();
 
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/VirtualHostConnectionLimiter.java b/broker-core/src/main/java/org/apache/qpid/server/virtualhost/VirtualHostConnectionLimiter.java
new file mode 100644
index 0000000..4188e1a
--- /dev/null
+++ b/broker-core/src/main/java/org/apache/qpid/server/virtualhost/VirtualHostConnectionLimiter.java
@@ -0,0 +1,285 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.virtualhost;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.qpid.server.model.AbstractConfigurationChangeListener;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.model.BrokerConnectionLimitProvider;
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.State;
+import org.apache.qpid.server.model.SystemConfig;
+import org.apache.qpid.server.model.VirtualHost;
+import org.apache.qpid.server.model.VirtualHostConnectionLimitProvider;
+import org.apache.qpid.server.security.limit.CachedConnectionLimiterImpl;
+import org.apache.qpid.server.security.limit.ConnectionLimitProvider;
+import org.apache.qpid.server.security.limit.ConnectionLimiter;
+import org.apache.qpid.server.security.limit.ConnectionLimiter.CachedLimiter;
+
+final class VirtualHostConnectionLimiter extends CachedConnectionLimiterImpl implements CachedLimiter
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(VirtualHostConnectionLimiter.class);
+
+    private final VirtualHost<?> _virtualHost;
+
+    private final Broker<?> _broker;
+
+    private final Map<ConnectionLimitProvider<?>, ConnectionLimiter> _connectionLimitProviders = new ConcurrentHashMap<>();
+
+    private final ChangeListener _virtualHostChangeListener;
+    private final ChangeListener _brokerChangeListener;
+
+    VirtualHostConnectionLimiter(VirtualHost<?> virtualHost, Broker<?> broker)
+    {
+        super(ConnectionLimiter.noLimits());
+        _virtualHost = Objects.requireNonNull(virtualHost);
+        _broker = Objects.requireNonNull(broker);
+        _virtualHostChangeListener = ChangeListener.virtualHostChangeListener(this);
+        _brokerChangeListener = ChangeListener.brokerChangeListener(this);
+    }
+
+    public void open()
+    {
+        _virtualHost.addChangeListener(_virtualHostChangeListener);
+        _broker.addChangeListener(_brokerChangeListener);
+
+        _virtualHost.getChildren(VirtualHostConnectionLimitProvider.class)
+                .forEach(child -> child.addChangeListener(ProviderChangeListener.virtualHostChangeListener(this)));
+        _broker.getChildren(BrokerConnectionLimitProvider.class)
+                .forEach(child -> child.addChangeListener(ProviderChangeListener.brokerChangeListener(this)));
+    }
+
+    public void activate()
+    {
+        update();
+    }
+
+    public void close()
+    {
+        _virtualHost.removeChangeListener(_virtualHostChangeListener);
+        _broker.removeChangeListener(_brokerChangeListener);
+
+        final ProviderChangeListener virtualHostChangeListener = ProviderChangeListener.virtualHostChangeListener(this);
+        _virtualHost.getChildren(VirtualHostConnectionLimitProvider.class)
+                .forEach(child -> child.removeChangeListener(virtualHostChangeListener));
+
+        final ProviderChangeListener brokerChangeListener = ProviderChangeListener.brokerChangeListener(this);
+        _broker.getChildren(BrokerConnectionLimitProvider.class)
+                .forEach(child -> child.removeChangeListener(brokerChangeListener));
+
+        swapLimiter(ConnectionLimiter.noLimits());
+    }
+
+    private void update(ConfiguredObject<?> object)
+    {
+        _connectionLimitProviders.remove(object);
+        update();
+    }
+
+    private void update()
+    {
+        if (!((SystemConfig<?>) _broker.getParent()).isManagementMode())
+        {
+            swapLimiter(newLimiter(_connectionLimitProviders));
+        }
+    }
+
+    private ConnectionLimiter newLimiter(final Map<ConnectionLimitProvider<?>, ConnectionLimiter> cache)
+    {
+        ConnectionLimiter limiter = ConnectionLimiter.noLimits();
+
+        LOGGER.debug("Updating virtual host connection limiters");
+        for (final VirtualHostConnectionLimitProvider<?> provider :
+                _virtualHost.getChildren(VirtualHostConnectionLimitProvider.class))
+        {
+            if (provider.getState() == State.ACTIVE)
+            {
+                limiter = limiter.append(
+                        cache.computeIfAbsent(provider, ConnectionLimitProvider::getConnectionLimiter));
+            }
+            else if (provider.getState() == State.ERRORED)
+            {
+                limiter = ConnectionLimiter.blockEveryone();
+            }
+        }
+
+        LOGGER.debug("Updating broker connection limiters");
+        for (final BrokerConnectionLimitProvider<?> provider :
+                _broker.getChildren(BrokerConnectionLimitProvider.class))
+        {
+            if (provider.getState() == State.ACTIVE)
+            {
+                limiter = limiter.append(
+                        cache.computeIfAbsent(provider, ConnectionLimitProvider::getConnectionLimiter));
+            }
+            else if (provider.getState() == State.ERRORED)
+            {
+                limiter = ConnectionLimiter.blockEveryone();
+            }
+        }
+        return limiter;
+    }
+
+    private abstract static class AbstractChangeListener extends AbstractConfigurationChangeListener
+    {
+        final VirtualHostConnectionLimiter _limiter;
+
+        final Class<?> _providerClazz;
+
+        AbstractChangeListener(VirtualHostConnectionLimiter limiter, Class<?> providerClazz)
+        {
+            super();
+            _limiter = Objects.requireNonNull(limiter);
+            _providerClazz = Objects.requireNonNull(providerClazz);
+        }
+
+        void addProvider(ConfiguredObject<?> provider)
+        {
+            provider.addChangeListener(new ProviderChangeListener(_limiter, _providerClazz));
+            _limiter.update();
+        }
+
+        void removeProvider(ConfiguredObject<?> provider)
+        {
+            provider.removeChangeListener(new ProviderChangeListener(_limiter, _providerClazz));
+            _limiter.update(provider);
+        }
+
+        void updateProvider(ConfiguredObject<?> provider)
+        {
+            _limiter.update(provider);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return 31 * _limiter.hashCode() + _providerClazz.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object obj)
+        {
+            if (obj instanceof AbstractChangeListener)
+            {
+                final AbstractChangeListener changeListener = (AbstractChangeListener) obj;
+                return _limiter == changeListener._limiter && _providerClazz == changeListener._providerClazz;
+            }
+            return false;
+        }
+    }
+
+    private static final class ChangeListener extends AbstractChangeListener
+    {
+        private final Class<?> _categoryClass;
+
+        static ChangeListener virtualHostChangeListener(VirtualHostConnectionLimiter limiter)
+        {
+            return new ChangeListener(limiter, VirtualHost.class, VirtualHostConnectionLimitProvider.class);
+        }
+
+        static ChangeListener brokerChangeListener(VirtualHostConnectionLimiter limiter)
+        {
+            return new ChangeListener(limiter, Broker.class, BrokerConnectionLimitProvider.class);
+        }
+
+        private ChangeListener(VirtualHostConnectionLimiter limiter,
+                               Class<?> categoryClass, Class<?> childCategoryClass)
+        {
+            super(limiter, childCategoryClass);
+            _categoryClass = categoryClass;
+        }
+
+        @Override
+        public void childAdded(final ConfiguredObject<?> object, final ConfiguredObject<?> child)
+        {
+            super.childAdded(object, child);
+            if (object.getCategoryClass() == _categoryClass && child.getCategoryClass() == _providerClazz)
+            {
+                addProvider(child);
+            }
+        }
+
+        @Override
+        public void childRemoved(final ConfiguredObject<?> object, final ConfiguredObject<?> child)
+        {
+            super.childRemoved(object, child);
+            if (object.getCategoryClass() == _categoryClass && child.getCategoryClass() == _providerClazz)
+            {
+                removeProvider(child);
+            }
+        }
+    }
+
+    private static final class ProviderChangeListener extends AbstractChangeListener
+    {
+        private final Map<ConfiguredObject<?>, Boolean> _bulkChanges = new ConcurrentHashMap<>();
+
+        static ProviderChangeListener virtualHostChangeListener(VirtualHostConnectionLimiter limiter)
+        {
+            return new ProviderChangeListener(limiter, VirtualHostConnectionLimitProvider.class);
+        }
+
+        static ProviderChangeListener brokerChangeListener(VirtualHostConnectionLimiter limiter)
+        {
+            return new ProviderChangeListener(limiter, BrokerConnectionLimitProvider.class);
+        }
+
+        ProviderChangeListener(VirtualHostConnectionLimiter limiter, Class<?> clazz)
+        {
+            super(limiter, clazz);
+        }
+
+        @Override
+        public void attributeSet(final ConfiguredObject<?> object,
+                                 final String attributeName,
+                                 final Object oldAttributeValue,
+                                 final Object newAttributeValue)
+        {
+            super.attributeSet(object, attributeName, oldAttributeValue, newAttributeValue);
+            if (object.getCategoryClass() == _providerClazz && !_bulkChanges.containsKey(object))
+            {
+                updateProvider(object);
+            }
+        }
+
+        @Override
+        public void bulkChangeStart(final ConfiguredObject<?> object)
+        {
+            super.bulkChangeStart(object);
+            _bulkChanges.put(object, Boolean.TRUE);
+        }
+
+        @Override
+        public void bulkChangeEnd(final ConfiguredObject<?> object)
+        {
+            super.bulkChangeEnd(object);
+            if (Optional.ofNullable(_bulkChanges.remove(object)).orElse(Boolean.FALSE))
+            {
+                updateProvider(object);
+            }
+        }
+    }
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/connection/ConnectionPrincipalStatisticsImpl.java b/broker-core/src/main/java/org/apache/qpid/server/virtualhost/connection/ConnectionPrincipalStatisticsImpl.java
deleted file mode 100644
index 9acd7a5..0000000
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/connection/ConnectionPrincipalStatisticsImpl.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.qpid.server.virtualhost.connection;
-
-import java.util.Collections;
-import java.util.List;
-
-import org.apache.qpid.server.virtualhost.ConnectionPrincipalStatistics;
-
-class ConnectionPrincipalStatisticsImpl implements ConnectionPrincipalStatistics
-{
-    private final int _connectionCount;
-    private final List<Long> _latestConnectionCreatedTimes;
-
-    ConnectionPrincipalStatisticsImpl(final int connectionCount, final List<Long> latestConnectionCreatedTimes)
-    {
-        _connectionCount = connectionCount;
-        _latestConnectionCreatedTimes = Collections.unmodifiableList(latestConnectionCreatedTimes);
-    }
-
-    @Override
-    public int getConnectionCount()
-    {
-        return _connectionCount;
-    }
-
-    @Override
-    public int getConnectionFrequency()
-    {
-        return _latestConnectionCreatedTimes.size();
-    }
-
-    List<Long> getLatestConnectionCreatedTimes()
-    {
-        return _latestConnectionCreatedTimes;
-    }
-
-    @Override
-    public boolean equals(final Object o)
-    {
-        if (this == o)
-        {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass())
-        {
-            return false;
-        }
-
-        final ConnectionPrincipalStatisticsImpl that = (ConnectionPrincipalStatisticsImpl) o;
-
-        if (_connectionCount != that._connectionCount)
-        {
-            return false;
-        }
-        return _latestConnectionCreatedTimes.equals(that._latestConnectionCreatedTimes);
-    }
-
-    @Override
-    public int hashCode()
-    {
-        int result = _connectionCount;
-        result = 31 * result + _latestConnectionCreatedTimes.hashCode();
-        return result;
-    }
-}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/connection/ConnectionPrincipalStatisticsRegistryImpl.java b/broker-core/src/main/java/org/apache/qpid/server/virtualhost/connection/ConnectionPrincipalStatisticsRegistryImpl.java
deleted file mode 100644
index 59d03ea..0000000
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/connection/ConnectionPrincipalStatisticsRegistryImpl.java
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.qpid.server.virtualhost.connection;
-
-import java.security.Principal;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
-
-import javax.security.auth.Subject;
-
-import org.apache.qpid.server.security.auth.AuthenticatedPrincipal;
-import org.apache.qpid.server.transport.AMQPConnection;
-import org.apache.qpid.server.virtualhost.ConnectionPrincipalStatistics;
-import org.apache.qpid.server.virtualhost.ConnectionPrincipalStatisticsRegistry;
-import org.apache.qpid.server.virtualhost.ConnectionStatisticsRegistrySettings;
-
-public class ConnectionPrincipalStatisticsRegistryImpl implements ConnectionPrincipalStatisticsRegistry
-{
-    private final Map<Principal, ConnectionPrincipalStatisticsImpl> _principalStatistics = new ConcurrentHashMap<>();
-    private final ConnectionStatisticsRegistrySettings _settings;
-
-    public ConnectionPrincipalStatisticsRegistryImpl(final ConnectionStatisticsRegistrySettings settings)
-    {
-        _settings = settings;
-    }
-
-    @Override
-    public ConnectionPrincipalStatistics connectionOpened(final AMQPConnection<?> connection)
-    {
-        final Subject subject = connection.getSubject();
-        final AuthenticatedPrincipal principal = AuthenticatedPrincipal.getAuthenticatedPrincipalFromSubject(subject);
-        return _principalStatistics.compute(principal,
-                                            (p, s) -> connectionOpened(s, connection.getCreatedTime()));
-    }
-
-    @Override
-    public ConnectionPrincipalStatistics connectionClosed(final AMQPConnection<?> connection)
-    {
-        final Subject subject = connection.getSubject();
-        final AuthenticatedPrincipal principal = AuthenticatedPrincipal.getAuthenticatedPrincipalFromSubject(subject);
-        return _principalStatistics.computeIfPresent(principal, (p, s) -> connectionClosed(s));
-    }
-
-    @Override
-    public void reevaluateConnectionStatistics()
-    {
-        new HashSet<>(_principalStatistics.keySet()).forEach(this::reevaluateConnectionStatistics);
-    }
-
-    @Override
-    public void reset()
-    {
-        _principalStatistics.clear();
-    }
-
-    int getConnectionCount(final Principal principal)
-    {
-        ConnectionPrincipalStatistics cs = _principalStatistics.get(principal);
-        if (cs != null)
-        {
-            return cs.getConnectionCount();
-        }
-        return 0;
-    }
-
-    int getConnectionFrequency(final Principal principal)
-    {
-        ConnectionPrincipalStatistics cs = _principalStatistics.get(principal);
-        if (cs != null)
-        {
-            return cs.getConnectionFrequency();
-        }
-        return 0;
-    }
-
-    private ConnectionPrincipalStatisticsImpl connectionOpened(final ConnectionPrincipalStatisticsImpl current,
-                                                           final Date createdTime)
-    {
-        if (current == null)
-        {
-            return new ConnectionPrincipalStatisticsImpl(1, Collections.singletonList(createdTime.getTime()));
-        }
-        else
-        {
-            final long frequencyPeriod = getConnectionFrequencyPeriodMillis();
-            final List<Long> connectionCreatedTimes;
-            if (frequencyPeriod > 0)
-            {
-                connectionCreatedTimes = findTimesWithinPeriod(current.getLatestConnectionCreatedTimes(), frequencyPeriod);
-                connectionCreatedTimes.add(createdTime.getTime());
-            }
-            else
-            {
-                connectionCreatedTimes = Collections.emptyList();
-            }
-            return new ConnectionPrincipalStatisticsImpl(current.getConnectionCount() + 1, connectionCreatedTimes);
-        }
-    }
-
-    private ConnectionPrincipalStatisticsImpl connectionClosed(final ConnectionPrincipalStatisticsImpl current)
-    {
-        return createStatisticsOrNull(Math.max(0, current.getConnectionCount() - 1), current.getLatestConnectionCreatedTimes());
-    }
-
-    private void reevaluateConnectionStatistics(final Principal authenticatedPrincipal)
-    {
-        _principalStatistics.computeIfPresent(authenticatedPrincipal, (principal, current) -> reevaluate(current));
-    }
-
-    private ConnectionPrincipalStatisticsImpl reevaluate(final ConnectionPrincipalStatisticsImpl current)
-    {
-        return createStatisticsOrNull(current.getConnectionCount(), current.getLatestConnectionCreatedTimes());
-    }
-
-    private ConnectionPrincipalStatisticsImpl createStatisticsOrNull(final int openConnectionCount,
-                                                                 final List<Long> currentConnectionCreatedTimes)
-    {
-        final List<Long> connectionCreatedTimes = findTimesWithinPeriod(currentConnectionCreatedTimes, getConnectionFrequencyPeriodMillis());
-        if (openConnectionCount == 0 && connectionCreatedTimes.isEmpty())
-        {
-            return null;
-        }
-        return new ConnectionPrincipalStatisticsImpl(openConnectionCount, connectionCreatedTimes);
-    }
-
-    private List<Long> findTimesWithinPeriod(final Collection<Long> currentConnectionCreatedTimes,
-                                             final long frequencyPeriod)
-    {
-
-        final List<Long> connectionCreatedTimes;
-        if (frequencyPeriod > 0)
-        {
-            final long periodEnd = System.currentTimeMillis();
-            final long periodStart = periodEnd - frequencyPeriod;
-            connectionCreatedTimes = findTimesBetween(currentConnectionCreatedTimes, periodStart, periodEnd);
-        }
-        else
-        {
-            connectionCreatedTimes = Collections.emptyList();
-        }
-        return connectionCreatedTimes;
-    }
-
-    private List<Long> findTimesBetween(final Collection<Long> connectionCreatedTimes,
-                                        final long periodStart,
-                                        final long periodEnd)
-    {
-        return connectionCreatedTimes.stream().mapToLong(Long::longValue)
-                                     .filter(t -> t >= periodStart && t <= periodEnd)
-                                     .boxed()
-                                     .collect(Collectors.toCollection(ArrayList::new));
-    }
-
-    private long getConnectionFrequencyPeriodMillis()
-    {
-        return _settings.getConnectionFrequencyPeriod().toMillis();
-    }
-}
diff --git a/broker-core/src/test/java/org/apache/qpid/server/model/VirtualHostTest.java b/broker-core/src/test/java/org/apache/qpid/server/model/VirtualHostTest.java
index 879d87c..a5f2e21 100644
--- a/broker-core/src/test/java/org/apache/qpid/server/model/VirtualHostTest.java
+++ b/broker-core/src/test/java/org/apache/qpid/server/model/VirtualHostTest.java
@@ -77,7 +77,6 @@
 import org.apache.qpid.server.store.preferences.PreferenceStore;
 import org.apache.qpid.server.store.preferences.PreferenceStoreUpdater;
 import org.apache.qpid.server.transport.AMQPConnection;
-import org.apache.qpid.server.virtualhost.ConnectionPrincipalStatistics;
 import org.apache.qpid.server.virtualhost.NodeAutoCreationPolicy;
 import org.apache.qpid.server.virtualhost.NoopConnectionEstablishmentPolicy;
 import org.apache.qpid.server.virtualhost.QueueManagingVirtualHost;
@@ -587,46 +586,6 @@
     }
 
     @Test
-    public void testRegisterConnectionCausesUpdateOfAuthenticatedPrincipalConnectionCountAndFrequency()
-    {
-        final NoopConnectionEstablishmentPolicy policy = new NoopConnectionEstablishmentPolicy();
-        final QueueManagingVirtualHost<?> vhost = createVirtualHost(getTestName());
-        final Principal principal = mockAuthenticatedPrincipal(getTestName());
-        final Principal principal2 = mockAuthenticatedPrincipal(getTestName() + "_2");
-        final AMQPConnection<?> connection1 = mockAmqpConnection(principal);
-        final AMQPConnection<?> connection2 = mockAmqpConnection(principal);
-        final AMQPConnection<?> connection3 = mockAmqpConnection(principal2);
-
-        vhost.registerConnection(connection1, policy);
-        verify(connection1).registered(argThat(new ConnectionPrincipalStatisticsArgumentMatcher(1, 1)));
-
-        vhost.registerConnection(connection2, policy);
-        verify(connection2).registered(argThat(new ConnectionPrincipalStatisticsArgumentMatcher(2, 2)));
-
-        vhost.registerConnection(connection3, policy);
-        verify(connection3).registered(argThat(new ConnectionPrincipalStatisticsArgumentMatcher(1, 1)));
-    }
-
-    @Test
-    public void testDeregisterConnectionAffectsAuthenticatedPrincipalConnectionCountAndFrequency()
-    {
-        final Principal principal = mockAuthenticatedPrincipal(getTestName());
-        final NoopConnectionEstablishmentPolicy policy = new NoopConnectionEstablishmentPolicy();
-        final QueueManagingVirtualHost<?> vhost = createVirtualHost(getTestName());
-
-        final AMQPConnection<?> connection1 = mockAmqpConnection(principal);
-        final AMQPConnection<?> connection2 = mockAmqpConnection(principal);
-
-        vhost.registerConnection(connection1, policy);
-        verify(connection1).registered(argThat(new ConnectionPrincipalStatisticsArgumentMatcher(1, 1)));
-
-        vhost.deregisterConnection(connection1);
-        vhost.registerConnection(connection2, policy);
-
-        verify(connection2).registered(argThat(new ConnectionPrincipalStatisticsArgumentMatcher(1, 2)));
-    }
-
-    @Test
     public void testStopVirtualhostClosesConnections()
     {
         QueueManagingVirtualHost<?> vhost = createVirtualHost("sdf");
@@ -879,25 +838,4 @@
             return (_id.equals(rhs.getId()) || _type.equals(rhs.getType()));
         }
     }
-
-    private static class ConnectionPrincipalStatisticsArgumentMatcher implements ArgumentMatcher<ConnectionPrincipalStatistics>
-    {
-
-        private final int _expectedConnectionCount;
-        private final int _expectedConnectionFrequency;
-
-        ConnectionPrincipalStatisticsArgumentMatcher(final int expectedConnectionCount,
-                                                     final int expectedConnectionFrequency)
-        {
-            _expectedConnectionCount = expectedConnectionCount;
-            _expectedConnectionFrequency = expectedConnectionFrequency;
-        }
-
-        @Override
-        public boolean matches(final ConnectionPrincipalStatistics connectionPrincipalStatistics)
-        {
-            return connectionPrincipalStatistics.getConnectionFrequency() == _expectedConnectionFrequency
-                    && connectionPrincipalStatistics.getConnectionCount() == _expectedConnectionCount;
-        }
-    }
 }
diff --git a/broker-core/src/test/java/org/apache/qpid/server/security/limit/ConnectionLimiterTest.java b/broker-core/src/test/java/org/apache/qpid/server/security/limit/ConnectionLimiterTest.java
new file mode 100644
index 0000000..70f82e8
--- /dev/null
+++ b/broker-core/src/test/java/org/apache/qpid/server/security/limit/ConnectionLimiterTest.java
@@ -0,0 +1,274 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.security.limit;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import org.apache.qpid.server.security.limit.ConnectionLimiter.CachedLimiter;
+import org.apache.qpid.server.transport.AMQPConnection;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class ConnectionLimiterTest extends UnitTestBase
+{
+    private static final String CONNECTION_BREAKS_LIMIT = "Connection breaks limit";
+
+    @Test
+    public void testRegister_NoLimits()
+    {
+        final ConnectionLimiter limiter = ConnectionLimiter.noLimits();
+        final AMQPConnection<?> connection = newConnection();
+        final ConnectionSlot slot = limiter.register(connection);
+
+        for (int i = 0; i < 127; i++)
+        {
+            assertEquals(slot, limiter.register(connection));
+        }
+    }
+
+    @Test
+    public void testDeregister_NoLimits()
+    {
+        final CachedLimiter limiter = ConnectionLimiter.noLimits();
+        final AMQPConnection<?> connection = newConnection();
+        limiter.register(connection);
+
+        for (int i = 0; i < 127; i++)
+        {
+            assertTrue(limiter.deregister(connection));
+        }
+    }
+
+    @Test
+    public void testRegister_BlockedUser()
+    {
+        final ConnectionLimiter limiter = ConnectionLimiter.blockEveryone();
+        for (int i = 0; i < 7; i++)
+        {
+            try
+            {
+                limiter.register(newConnection());
+                fail("A connection limit exception is expected");
+            }
+            catch (ConnectionLimitException e)
+            {
+                assertNotNull(e.getMessage());
+            }
+        }
+    }
+
+    @Test
+    public void testDeregister_BlockedUser()
+    {
+        final CachedLimiter limiter = ConnectionLimiter.blockEveryone();
+        final AMQPConnection<?> connection = newConnection();
+        try
+        {
+            limiter.register(connection);
+            fail("A connection limit exception is expected");
+        }
+        catch (ConnectionLimitException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+        for (int i = 0; i < 7; i++)
+        {
+            assertFalse(limiter.deregister(connection));
+        }
+    }
+
+    @Test
+    public void testRegister_CachedLimiter()
+    {
+        final ConnectionLimiterImpl underlyingLimiter = new ConnectionLimiterImpl(1);
+        final CachedLimiter limiter = new CachedConnectionLimiterImpl(underlyingLimiter);
+
+        final AMQPConnection<?> connection = newConnection();
+        final ConnectionSlot slot1 = limiter.register(connection);
+        assertEquals(slot1, limiter.register(connection));
+        assertEquals(slot1, limiter.register(connection));
+        slot1.free();
+
+        final ConnectionSlot slot2 = limiter.register(connection);
+        assertNotEquals(slot1, slot2);
+        assertEquals(slot2, limiter.register(connection));
+        assertEquals(slot2, limiter.register(connection));
+
+        slot1.free();
+        try
+        {
+            underlyingLimiter.register(connection).free();
+            fail("A connection limit exception is expected");
+        }
+        catch (ConnectionLimitException e)
+        {
+            assertEquals(CONNECTION_BREAKS_LIMIT, e.getMessage());
+        }
+
+        slot2.free();
+        underlyingLimiter.register(connection).free();
+    }
+
+    @Test
+    public void testDeregister_CachedLimiter()
+    {
+        final CachedLimiter limiter = new CachedConnectionLimiterImpl(new ConnectionLimiterImpl(1));
+        final AMQPConnection<?> connection = newConnection();
+        limiter.register(connection);
+
+        assertTrue(limiter.deregister(connection));
+        assertFalse(limiter.deregister(connection));
+        assertFalse(limiter.deregister(connection));
+    }
+
+    @Test
+    public void testAppend_noLimits()
+    {
+        final ConnectionLimiter secondary = new ConnectionLimiterImpl(1);
+        final ConnectionLimiter noLimits = ConnectionLimiter.noLimits();
+
+        final ConnectionLimiter limiter = noLimits.append(secondary);
+        final AMQPConnection<?> connection = newConnection();
+        final ConnectionSlot slot = limiter.register(connection);
+
+        try
+        {
+            limiter.register(connection);
+            fail("A connection limit exception is expected here");
+        }
+        catch (ConnectionLimitException e)
+        {
+            assertEquals(CONNECTION_BREAKS_LIMIT, e.getMessage());
+        }
+        slot.free();
+        limiter.register(connection).free();
+    }
+
+    @Test
+    public void testAppend_blockEveryone()
+    {
+        final ConnectionLimiter secondary = new ConnectionLimiterImpl(1);
+        final ConnectionLimiter blocked = ConnectionLimiter.blockEveryone();
+
+        final ConnectionLimiter limiter = blocked.append(secondary);
+
+        for (int i = 0; i < 3; i++)
+        {
+            try
+            {
+                limiter.register(newConnection());
+                fail("A connection limit exception is expected here");
+            }
+            catch (ConnectionLimitException e)
+            {
+                assertNotNull(e.getMessage());
+            }
+        }
+    }
+
+    @Test
+    public void testAppend_CachedLimiter()
+    {
+        final ConnectionLimiter secondary = new ConnectionLimiterImpl(1);
+        final ConnectionLimiter cachedLimiter = new CachedConnectionLimiterImpl(new ConnectionLimiterImpl(10));
+
+        final ConnectionLimiter limiter = cachedLimiter.append(secondary);
+        final AMQPConnection<?> connection = newConnection();
+        final ConnectionSlot slot = limiter.register(connection);
+
+        try
+        {
+            limiter.register(connection);
+            fail("A connection limit exception is expected here");
+        }
+        catch (ConnectionLimitException e)
+        {
+            assertEquals(CONNECTION_BREAKS_LIMIT, e.getMessage());
+        }
+        slot.free();
+        limiter.register(connection).free();
+    }
+
+    @Test
+    public void testFreeSlot_NoLimits()
+    {
+        final ConnectionLimiter limiter = ConnectionLimiter.noLimits();
+
+        for (int i = 0; i < 127; i++)
+        {
+            limiter.register(newConnection()).free();
+        }
+    }
+
+    private AMQPConnection<?> newConnection()
+    {
+        return Mockito.mock(AMQPConnection.class);
+    }
+
+    private static final class ConnectionLimiterImpl implements ConnectionLimiter
+    {
+        private final Map<AMQPConnection<?>, Integer> _counters;
+        private final int _limit;
+        private final ConnectionLimiter _subLimiter;
+
+        public ConnectionLimiterImpl(int limit)
+        {
+            _counters = new HashMap<>();
+            _limit = limit;
+            _subLimiter = ConnectionLimiter.noLimits();
+        }
+
+        private ConnectionLimiterImpl(ConnectionLimiterImpl limiter, ConnectionLimiter subLimiter)
+        {
+            _counters = limiter._counters;
+            _limit = limiter._limit;
+            _subLimiter = subLimiter;
+        }
+
+        @Override
+        public ConnectionSlot register(final AMQPConnection<?> connection)
+        {
+            int counter = _counters.computeIfAbsent(connection, con -> 0);
+            if (counter >= _limit)
+            {
+                throw new ConnectionLimitException(CONNECTION_BREAKS_LIMIT);
+            }
+            final ConnectionSlot subSlot = _subLimiter.register(connection);
+            _counters.put(connection, counter + 1);
+            final ConnectionSlot slot = () -> _counters.put(connection, _counters.get(connection) - 1);
+            return slot.chainTo(subSlot);
+        }
+
+        @Override
+        public ConnectionLimiter append(ConnectionLimiter limiter)
+        {
+            return new ConnectionLimiterImpl(this, _subLimiter.append(limiter));
+        }
+    }
+}
\ No newline at end of file
diff --git a/broker-core/src/test/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatisticsCheckingTaskTest.java b/broker-core/src/test/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatisticsCheckingTaskTest.java
deleted file mode 100644
index 1764050..0000000
--- a/broker-core/src/test/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatisticsCheckingTaskTest.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.qpid.server.virtualhost;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import java.security.AccessController;
-
-import org.junit.Test;
-
-import org.apache.qpid.test.utils.UnitTestBase;
-
-public class ConnectionPrincipalStatisticsCheckingTaskTest extends UnitTestBase
-{
-    @Test
-    public void execute()
-    {
-        final QueueManagingVirtualHost vh = mock(QueueManagingVirtualHost.class);
-        when(vh.getName()).thenReturn(getTestName());
-        final ConnectionPrincipalStatisticsRegistry registry = mock(ConnectionPrincipalStatisticsRegistry.class);
-        ConnectionPrincipalStatisticsCheckingTask task =
-                new ConnectionPrincipalStatisticsCheckingTask(vh, AccessController.getContext(), registry);
-
-        task.execute();
-
-        verify(registry).reevaluateConnectionStatistics();
-    }
-}
diff --git a/broker-core/src/test/java/org/apache/qpid/server/virtualhost/connection/ConnectionPrincipalStatisticsImplTest.java b/broker-core/src/test/java/org/apache/qpid/server/virtualhost/connection/ConnectionPrincipalStatisticsImplTest.java
deleted file mode 100644
index fd08684..0000000
--- a/broker-core/src/test/java/org/apache/qpid/server/virtualhost/connection/ConnectionPrincipalStatisticsImplTest.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.qpid.server.virtualhost.connection;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import org.junit.Test;
-
-import org.apache.qpid.server.virtualhost.ConnectionPrincipalStatistics;
-import org.apache.qpid.test.utils.UnitTestBase;
-
-public class ConnectionPrincipalStatisticsImplTest extends UnitTestBase
-{
-
-    @Test
-    public void getOpenConnectionCount()
-    {
-        final ConnectionPrincipalStatistics stats =
-                new ConnectionPrincipalStatisticsImpl(1, Collections.singletonList(System.currentTimeMillis()));
-
-        assertEquals(1, stats.getConnectionCount());
-    }
-
-    @Test
-    public void getOpenConnectionFrequency()
-    {
-        final long connectionCreatedTime = System.currentTimeMillis();
-        final ConnectionPrincipalStatistics stats =
-                new ConnectionPrincipalStatisticsImpl(1,
-                                                      Arrays.asList(connectionCreatedTime - 1000, connectionCreatedTime));
-        assertEquals(2, stats.getConnectionFrequency());
-    }
-
-    @Test
-    public void getLatestConnectionCreatedTimes()
-    {
-        final long connectionCreatedTime = System.currentTimeMillis();
-        final List<Long> connectionCreatedTimes = Arrays.asList(connectionCreatedTime - 1000, connectionCreatedTime);
-        final ConnectionPrincipalStatisticsImpl stats = new ConnectionPrincipalStatisticsImpl(1, connectionCreatedTimes);
-        assertEquals(connectionCreatedTimes, stats.getLatestConnectionCreatedTimes());
-    }
-
-    @Test
-    public void equals()
-    {
-        final long connectionCreatedTime = System.currentTimeMillis();
-        final ConnectionPrincipalStatistics stats1 =
-                new ConnectionPrincipalStatisticsImpl(1, Collections.singletonList(connectionCreatedTime));
-        final ConnectionPrincipalStatistics stats2 =
-                new ConnectionPrincipalStatisticsImpl(1, Collections.singletonList(connectionCreatedTime));
-        assertEquals(stats1, stats2);
-
-        final long connectionCreatedTime2 = System.currentTimeMillis();
-        final ConnectionPrincipalStatistics stats3 =
-                new ConnectionPrincipalStatisticsImpl(2, Arrays.asList(connectionCreatedTime, connectionCreatedTime2));
-
-        assertNotEquals(stats2, stats3);
-        assertNotEquals(stats1, stats3);
-    }
-
-    @Test
-    public void testHashCode()
-    {
-        final long connectionCreatedTime = System.currentTimeMillis();
-        final ConnectionPrincipalStatistics stats1 =
-                new ConnectionPrincipalStatisticsImpl(1, Collections.singletonList(connectionCreatedTime));
-        final ConnectionPrincipalStatistics stats2 =
-                new ConnectionPrincipalStatisticsImpl(1, Collections.singletonList(connectionCreatedTime));
-        assertEquals(stats1.hashCode(), stats2.hashCode());
-
-        final long connectionCreatedTime2 = System.currentTimeMillis();
-        final ConnectionPrincipalStatistics stats3 =
-                new ConnectionPrincipalStatisticsImpl(2, Arrays.asList(connectionCreatedTime, connectionCreatedTime2));
-
-        assertNotEquals(stats2.hashCode(), stats3.hashCode());
-        assertNotEquals(stats1.hashCode(), stats3.hashCode());
-    }
-}
diff --git a/broker-core/src/test/java/org/apache/qpid/server/virtualhost/connection/ConnectionPrincipalStatisticsRegistryImplTest.java b/broker-core/src/test/java/org/apache/qpid/server/virtualhost/connection/ConnectionPrincipalStatisticsRegistryImplTest.java
deleted file mode 100644
index 0c61b8e..0000000
--- a/broker-core/src/test/java/org/apache/qpid/server/virtualhost/connection/ConnectionPrincipalStatisticsRegistryImplTest.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.qpid.server.virtualhost.connection;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.security.Principal;
-import java.time.Duration;
-import java.util.Collections;
-import java.util.Date;
-
-import javax.security.auth.Subject;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-
-import org.apache.qpid.server.security.auth.AuthenticatedPrincipal;
-import org.apache.qpid.server.transport.AMQPConnection;
-import org.apache.qpid.server.virtualhost.ConnectionStatisticsRegistrySettings;
-import org.apache.qpid.test.utils.UnitTestBase;
-
-public class ConnectionPrincipalStatisticsRegistryImplTest extends UnitTestBase
-{
-    private static final Duration CONNECTION_FREQUENCY_PERIOD = Duration.ofMillis(5000);
-    private ConnectionPrincipalStatisticsRegistryImpl _statisticsRegistry;
-    private AuthenticatedPrincipal _authorizedPrincipal;
-    private ConnectionStatisticsRegistrySettings _settings;
-
-    @Before
-    public void setUp()
-    {
-        _settings = mock(ConnectionStatisticsRegistrySettings.class);
-        when(_settings.getConnectionFrequencyPeriod()).thenReturn(CONNECTION_FREQUENCY_PERIOD);
-        _statisticsRegistry = new ConnectionPrincipalStatisticsRegistryImpl(_settings);
-        _authorizedPrincipal = new AuthenticatedPrincipal(mock(Principal.class));
-    }
-
-    @Test
-    public void onConnectionOpen()
-    {
-        final AMQPConnection connection = mockConnection();
-
-        _statisticsRegistry.connectionOpened(connection);
-
-        assertThat(_statisticsRegistry.getConnectionCount(_authorizedPrincipal), is(equalTo(1)));
-        assertThat(_statisticsRegistry.getConnectionCount(_authorizedPrincipal), is(equalTo(1)));
-    }
-
-    @Test
-    public void onConnectionClose()
-    {
-        final AMQPConnection connection1 = mockConnection();
-
-        _statisticsRegistry.connectionOpened(connection1);
-        _statisticsRegistry.connectionClosed(connection1);
-
-        final AMQPConnection connection2 = mockConnection();
-        _statisticsRegistry.connectionOpened(connection2);
-
-        assertThat(_statisticsRegistry.getConnectionCount(_authorizedPrincipal), is(equalTo(1)));
-        assertThat(_statisticsRegistry.getConnectionFrequency(_authorizedPrincipal), is(equalTo(2)));
-    }
-
-    @Test
-    public void reevaluateConnectionPrincipalStatistics() throws InterruptedException
-    {
-        final AMQPConnection connection1 = mockConnection();
-
-        _statisticsRegistry.connectionOpened(connection1);
-        assertThat(_statisticsRegistry.getConnectionFrequency(_authorizedPrincipal), is(equalTo(1)));
-
-        _statisticsRegistry.reevaluateConnectionStatistics();
-        assertThat(_statisticsRegistry.getConnectionFrequency(_authorizedPrincipal), is(equalTo(1)));
-
-        when(_settings.getConnectionFrequencyPeriod()).thenReturn(Duration.ofMillis(1));
-        Thread.sleep(_settings.getConnectionFrequencyPeriod().toMillis() + 1);
-
-        _statisticsRegistry.reevaluateConnectionStatistics();
-        assertThat(_statisticsRegistry.getConnectionCount(_authorizedPrincipal), is(equalTo(1)));
-        assertThat(_statisticsRegistry.getConnectionFrequency(_authorizedPrincipal), is(equalTo(0)));
-    }
-
-    @Ignore
-    @Test
-    public void getConnectionFrequencyAfterExpirationOfFrequencyPeriod() throws InterruptedException
-    {
-        final AMQPConnection connection1 = mockConnection();
-        _statisticsRegistry.connectionOpened(connection1);
-
-        assertThat(_statisticsRegistry.getConnectionFrequency(_authorizedPrincipal), is(equalTo(1)));
-        assertThat(_statisticsRegistry.getConnectionCount(_authorizedPrincipal), is(equalTo(1)));
-
-        when(_settings.getConnectionFrequencyPeriod()).thenReturn(Duration.ofMillis(1));
-        Thread.sleep(_settings.getConnectionFrequencyPeriod().toMillis() + 1);
-
-        final AMQPConnection connection2 = mockConnection();
-        _statisticsRegistry.connectionOpened(connection2);
-
-        assertThat(_statisticsRegistry.getConnectionCount(_authorizedPrincipal), is(equalTo(2)));
-        assertThat(_statisticsRegistry.getConnectionFrequency(_authorizedPrincipal), is(equalTo(1)));
-    }
-
-    private AMQPConnection mockConnection()
-    {
-        final Subject subject = new Subject(true,
-                                            Collections.singleton(_authorizedPrincipal),
-                                            Collections.emptySet(),
-                                            Collections.emptySet());
-
-        final AMQPConnection connection = mock(AMQPConnection.class);
-        when(connection.getAuthorizedPrincipal()).thenReturn(_authorizedPrincipal);
-        when(connection.getSubject()).thenReturn(subject);
-        when(connection.getCreatedTime()).thenReturn(new Date());
-        return connection;
-    }
-}
diff --git a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclAction.java b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclAction.java
index 48452e2..4bff11c 100644
--- a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclAction.java
+++ b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclAction.java
@@ -44,9 +44,9 @@
         _action = new Action(operation, object, properties);
     }
 
-    public DynamicRule getDynamicRule()
+    public FirewallRule getFirewallRule()
     {
-        return _predicates == null ? null : _predicates.getDynamicRule();
+        return _predicates == null ? null : _predicates.getFirewallRule();
     }
 
     public Action getAction()
diff --git a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclRulePredicates.java b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclRulePredicates.java
index a172b01..c51cd57 100644
--- a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclRulePredicates.java
+++ b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/AclRulePredicates.java
@@ -18,20 +18,17 @@
  */
 package org.apache.qpid.server.security.access.config;
 
+import com.google.common.collect.Sets;
+import org.apache.qpid.server.security.access.config.ObjectProperties.Property;
+import org.apache.qpid.server.security.access.firewall.FirewallRuleFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.stream.Collectors;
 
-import com.google.common.collect.Sets;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import org.apache.qpid.server.security.access.config.ObjectProperties.Property;
-import org.apache.qpid.server.security.access.config.connection.ConnectionPrincipalFrequencyLimitRule;
-import org.apache.qpid.server.security.access.config.connection.ConnectionPrincipalLimitRule;
-import org.apache.qpid.server.security.access.firewall.FirewallRuleFactory;
-
 /**
  * Represents the predicates on an ACL rule by combining predicates relating to the object being operated on
  * (e.g. name=foo) with dynamic rules.
@@ -44,7 +41,7 @@
 
     private final ObjectProperties _properties = new ObjectProperties();
     private final Map<Property, String> _parsedProperties = new HashMap<>();
-    private volatile DynamicRule _dynamicRule = s -> true;
+    private volatile FirewallRule _firewallRule = s -> true;
     private volatile FirewallRuleFactory _firewallRuleFactory = new FirewallRuleFactory();
 
     public AclRulePredicates()
@@ -66,23 +63,25 @@
     {
         ObjectProperties.Property property = ObjectProperties.Property.parse(key);
 
-        addPropertyValue(property, value);
-        _parsedProperties.put(property, value);
-        LOGGER.debug("Parsed {} with value {}", property, value);
+        if (addPropertyValue(property, value))
+        {
+            _parsedProperties.put(property, value);
+            LOGGER.debug("Parsed {} with value {}", property, value);
+        }
     }
 
-    private void addPropertyValue(final Property property, final String value)
+    private boolean addPropertyValue(final Property property, final String value)
     {
-        final DynamicRule dynamicRule = _dynamicRule;
+        final FirewallRule firewallRule = _firewallRule;
         if (property == Property.FROM_HOSTNAME)
         {
             checkFirewallRuleNotAlreadyDefined(property, value, Property.FROM_NETWORK);
-            _dynamicRule = dynamicRule.and(_firewallRuleFactory.createForHostname(value.split(SEPARATOR)));
+            _firewallRule = firewallRule.and(_firewallRuleFactory.createForHostname(value.split(SEPARATOR)));
         }
         else if (property == Property.FROM_NETWORK)
         {
             checkFirewallRuleNotAlreadyDefined(property, value, Property.FROM_HOSTNAME);
-            _dynamicRule = dynamicRule.and(_firewallRuleFactory.createForNetwork(value.split(SEPARATOR)));
+            _firewallRule = firewallRule.and(_firewallRuleFactory.createForNetwork(value.split(SEPARATOR)));
         }
         else if (property == Property.ATTRIBUTES)
         {
@@ -90,34 +89,19 @@
         }
         else if (property == Property.CONNECTION_LIMIT)
         {
-            checkPropertyAlreadyDefined(property);
-            final int limit = getLimit(property, value);
-            _dynamicRule = dynamicRule.and(new ConnectionPrincipalLimitRule(limit));
+            LOGGER.warn("The ACL Rule property 'connection_limit' has been deprecated");
+            return false;
         }
         else if (property == Property.CONNECTION_FREQUENCY_LIMIT)
         {
-            checkPropertyAlreadyDefined(property);
-            final int limit = getLimit(property, value);
-            _dynamicRule = dynamicRule.and(new ConnectionPrincipalFrequencyLimitRule(limit));
+            LOGGER.warn("The ACL Rule property 'connection_frequency_limit' has been deprecated");
+            return false;
         }
         else
         {
             _properties.put(property, value);
         }
-    }
-
-    private int getLimit(final Property property, final String value)
-    {
-        int limit;
-        try
-        {
-            limit = Integer.parseInt(value);
-        }
-        catch (Exception e)
-        {
-            throw new IllegalStateException(String.format("Property '%s' value '%s' is not integer", property, value));
-        }
-        return limit;
+        return true;
     }
 
     private void checkFirewallRuleNotAlreadyDefined(Property property, String value, Property... exclusiveProperty)
@@ -177,9 +161,9 @@
                                               .collect(Collectors.joining(" ")));
     }
 
-    DynamicRule getDynamicRule()
+    FirewallRule getFirewallRule()
     {
-        return _dynamicRule;
+        return _firewallRule;
     }
 
     ObjectProperties getObjectProperties()
diff --git a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/ClientAction.java b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/ClientAction.java
index 23f0605..d010e78 100644
--- a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/ClientAction.java
+++ b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/ClientAction.java
@@ -41,10 +41,10 @@
     public boolean matches(AclAction ruleAction, final Subject subject)
     {
         return _clientAction.matches(ruleAction.getAction())
-               && dynamicMatches(ruleAction.getDynamicRule(), subject);
+               && dynamicMatches(ruleAction.getFirewallRule(), subject);
     }
 
-    private boolean dynamicMatches(final DynamicRule dynamicRule, final Subject subject)
+    private boolean dynamicMatches(final FirewallRule dynamicRule, final Subject subject)
     {
         return dynamicRule == null || dynamicRule.matches(subject);
     }
diff --git a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/DynamicRule.java b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/FirewallRule.java
similarity index 80%
rename from broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/DynamicRule.java
rename to broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/FirewallRule.java
index ede7d13..5227073 100644
--- a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/DynamicRule.java
+++ b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/FirewallRule.java
@@ -23,26 +23,26 @@
 
 import javax.security.auth.Subject;
 
-public interface DynamicRule
+public interface FirewallRule
 {
     boolean matches(Subject subject);
 
-    default DynamicRule and(DynamicRule other)
+    default FirewallRule and(FirewallRule other)
     {
         Objects.requireNonNull(other);
 
-        return new DynamicRule()
+        return new FirewallRule()
         {
             @Override
             public boolean matches(final Subject subject)
             {
-                return DynamicRule.this.matches(subject) && other.matches(subject);
+                return FirewallRule.this.matches(subject) && other.matches(subject);
             }
 
             @Override
             public String toString()
             {
-                return String.format("%s and %s", DynamicRule.this.toString(), other.toString());
+                return String.format("%s and %s", FirewallRule.this.toString(), other.toString());
             }
         };
     }
diff --git a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/LegacyAccessControlAdapter.java b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/LegacyAccessControlAdapter.java
index e89f5c0..dda4fa3 100644
--- a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/LegacyAccessControlAdapter.java
+++ b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/LegacyAccessControlAdapter.java
@@ -183,6 +183,7 @@
                VirtualHostLogger.class.isAssignableFrom(category) ||
                VirtualHostLogInclusionRule.class.isAssignableFrom(category) ||
                VirtualHostAccessControlProvider.class.isAssignableFrom(category) ||
+               VirtualHostConnectionLimitProvider.class.isAssignableFrom(category) ||
                Connection.class.isAssignableFrom(category);
     }
 
diff --git a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalFrequencyLimitRule.java b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalFrequencyLimitRule.java
deleted file mode 100644
index 8f4e7e4..0000000
--- a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalFrequencyLimitRule.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.qpid.server.security.access.config.connection;
-
-import org.apache.qpid.server.security.access.config.ObjectProperties;
-import org.apache.qpid.server.transport.AMQPConnection;
-
-public class ConnectionPrincipalFrequencyLimitRule extends ConnectionPrincipalStatisticsRule
-{
-    public ConnectionPrincipalFrequencyLimitRule(final int limit)
-    {
-        super(limit);
-    }
-
-    @Override
-    boolean matches(final AMQPConnection<?> connection)
-    {
-        return connection.getAuthenticatedPrincipalConnectionFrequency() <= getLimit();
-    }
-
-    @Override
-    public String toString()
-    {
-        return String.format("ConnectionPrincipalFrequencyLimitRule{%s=%d}",
-                             ObjectProperties.Property.CONNECTION_FREQUENCY_LIMIT.name().toLowerCase(),
-                             getLimit());
-    }
-}
diff --git a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalLimitRule.java b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalLimitRule.java
deleted file mode 100644
index 94a93f5..0000000
--- a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalLimitRule.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.qpid.server.security.access.config.connection;
-
-import org.apache.qpid.server.security.access.config.ObjectProperties;
-import org.apache.qpid.server.transport.AMQPConnection;
-
-public class ConnectionPrincipalLimitRule extends ConnectionPrincipalStatisticsRule
-{
-    public ConnectionPrincipalLimitRule(final int limit)
-    {
-        super(limit);
-    }
-
-    protected boolean matches(final AMQPConnection<?> connection)
-    {
-        return connection.getAuthenticatedPrincipalConnectionCount() <= getLimit();
-    }
-
-    @Override
-    public String toString()
-    {
-        return String.format("ConnectionPrincipalLimitRule{%s=%d}",
-                             ObjectProperties.Property.CONNECTION_LIMIT.name().toLowerCase(),
-                             getLimit());
-    }
-}
diff --git a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalStatisticsRule.java b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalStatisticsRule.java
deleted file mode 100644
index 466eb9f..0000000
--- a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalStatisticsRule.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.qpid.server.security.access.config.connection;
-
-import javax.security.auth.Subject;
-
-import org.apache.qpid.server.connection.ConnectionPrincipal;
-import org.apache.qpid.server.security.access.config.DynamicRule;
-import org.apache.qpid.server.transport.AMQPConnection;
-
-public abstract class ConnectionPrincipalStatisticsRule implements DynamicRule
-{
-    private final int _limit;
-
-    ConnectionPrincipalStatisticsRule(final int limit)
-    {
-        _limit = limit;
-    }
-
-    int getLimit()
-    {
-        return _limit;
-    }
-
-    @Override
-    public boolean matches(final Subject subject)
-    {
-        AMQPConnection<?> connection = getConnection(subject);
-        if (connection != null)
-        {
-            return matches(connection);
-        }
-        return false;
-    }
-
-    abstract boolean matches(final AMQPConnection<?> connection);
-
-    private AMQPConnection<?> getConnection(final Subject subject)
-    {
-        if (subject != null)
-        {
-            final ConnectionPrincipal principal = subject.getPrincipals(ConnectionPrincipal.class)
-                                                         .stream()
-                                                         .findFirst()
-                                                         .orElse(null);
-            if (principal != null)
-            {
-                return principal.getConnection();
-            }
-        }
-        return null;
-    }
-}
diff --git a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/FirewallRule.java b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/AbstractFirewallRuleImpl.java
similarity index 90%
rename from broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/FirewallRule.java
rename to broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/AbstractFirewallRuleImpl.java
index d627655..e699b7d 100644
--- a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/FirewallRule.java
+++ b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/AbstractFirewallRuleImpl.java
@@ -26,11 +26,15 @@
 import javax.security.auth.Subject;
 
 import org.apache.qpid.server.connection.ConnectionPrincipal;
-import org.apache.qpid.server.security.access.config.DynamicRule;
-import org.apache.qpid.server.security.access.config.ObjectProperties;
+import org.apache.qpid.server.security.access.config.FirewallRule;
 
-public abstract class FirewallRule implements DynamicRule
+abstract class AbstractFirewallRuleImpl implements FirewallRule
 {
+    AbstractFirewallRuleImpl()
+    {
+        super();
+    }
+
     @Override
     public boolean matches(final Subject subject)
     {
@@ -57,5 +61,4 @@
     }
 
     protected abstract boolean matches(InetAddress addressOfClient);
-
 }
diff --git a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/FirewallRuleFactory.java b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/FirewallRuleFactory.java
index 64be26c..8ab67f4 100644
--- a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/FirewallRuleFactory.java
+++ b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/FirewallRuleFactory.java
@@ -18,6 +18,8 @@
  */
 package org.apache.qpid.server.security.access.firewall;
 
+import org.apache.qpid.server.security.access.config.FirewallRule;
+
 public class FirewallRuleFactory
 {
     public FirewallRule createForHostname(String[] hostnames)
diff --git a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/HostnameFirewallRule.java b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/HostnameFirewallRule.java
index ac95a15..2e275a6 100644
--- a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/HostnameFirewallRule.java
+++ b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/HostnameFirewallRule.java
@@ -18,6 +18,9 @@
  */
 package org.apache.qpid.server.security.access.firewall;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.net.InetAddress;
 import java.util.Arrays;
 import java.util.concurrent.Callable;
@@ -27,10 +30,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Pattern;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class HostnameFirewallRule extends FirewallRule
+public class HostnameFirewallRule extends AbstractFirewallRuleImpl
 {
     private static final Logger LOGGER = LoggerFactory.getLogger(HostnameFirewallRule.class);
 
@@ -42,6 +42,7 @@
 
     public HostnameFirewallRule(String... hostnames)
     {
+        super();
         _hostnames = hostnames;
 
         int i = 0;
@@ -69,7 +70,7 @@
 
             if (hostnameMatches)
             {
-                if(LOGGER.isDebugEnabled())
+                if (LOGGER.isDebugEnabled())
                 {
                     LOGGER.debug("Hostname " + hostname + " matches rule " + pattern.toString());
                 }
@@ -83,10 +84,9 @@
     }
 
     /**
-     * @param remote
-     *            the InetAddress to look up
+     * @param remote the InetAddress to look up
      * @return the hostname, null if not found, takes longer than
-     *         {@link #DNS_LOOKUP} to find or otherwise fails
+     * {@link #DNS_LOOKUP} to find or otherwise fails
      */
     private String getHostname(final InetAddress remote) throws AccessControlFirewallException
     {
@@ -144,7 +144,7 @@
     public String toString()
     {
         return "HostnameFirewallRule[" +
-               "hostnames=" + Arrays.toString(_hostnames) +
-               ']';
+                "hostnames=" + Arrays.toString(_hostnames) +
+                ']';
     }
 }
diff --git a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/NetworkFirewallRule.java b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/NetworkFirewallRule.java
index e4d41e8..999b514 100644
--- a/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/NetworkFirewallRule.java
+++ b/broker-plugins/access-control/src/main/java/org/apache/qpid/server/security/access/firewall/NetworkFirewallRule.java
@@ -25,13 +25,14 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class NetworkFirewallRule extends FirewallRule
+public class NetworkFirewallRule extends AbstractFirewallRuleImpl
 {
     private static final Logger LOGGER = LoggerFactory.getLogger(NetworkFirewallRule.class);
     private List<InetNetwork> _networks;
 
     public NetworkFirewallRule(String... networks)
     {
+        super();
         _networks = new ArrayList<InetNetwork>();
         for (int i = 0; i < networks.length; i++)
         {
diff --git a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/AclActionTest.java b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/AclActionTest.java
index 45980aa..05986bb 100644
--- a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/AclActionTest.java
+++ b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/AclActionTest.java
@@ -20,7 +20,6 @@
 
 import static org.hamcrest.CoreMatchers.allOf;
 import static org.hamcrest.Matchers.aMapWithSize;
-import static org.hamcrest.Matchers.anEmptyMap;
 import static org.hamcrest.Matchers.hasEntry;
 import static org.junit.Assert.assertFalse;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -34,7 +33,6 @@
 import org.hamcrest.Matchers;
 import org.junit.Test;
 
-import org.apache.qpid.server.security.access.firewall.FirewallRule;
 import org.apache.qpid.test.utils.UnitTestBase;
 
 public class AclActionTest extends UnitTestBase
@@ -103,7 +101,7 @@
     private AclRulePredicates createAclRulePredicates()
     {
         AclRulePredicates predicates = mock(AclRulePredicates.class);
-        when(predicates.getDynamicRule()).thenReturn(mock(FirewallRule.class));
+        when(predicates.getFirewallRule()).thenReturn(mock(FirewallRule.class));
         when(predicates.getObjectProperties()).thenReturn(mock(ObjectProperties.class));
         return predicates;
     }
diff --git a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/AclRulePredicatesTest.java b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/AclRulePredicatesTest.java
index b40fe8d..72d9ad4 100644
--- a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/AclRulePredicatesTest.java
+++ b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/AclRulePredicatesTest.java
@@ -20,7 +20,6 @@
 
 import static org.apache.qpid.server.security.access.config.ObjectProperties.Property.ATTRIBUTES;
 import static org.apache.qpid.server.security.access.config.ObjectProperties.Property.CLASS;
-import static org.apache.qpid.server.security.access.config.ObjectProperties.Property.CONNECTION_LIMIT;
 import static org.apache.qpid.server.security.access.config.ObjectProperties.Property.FROM_HOSTNAME;
 import static org.apache.qpid.server.security.access.config.ObjectProperties.Property.FROM_NETWORK;
 import static org.apache.qpid.server.security.access.config.ObjectProperties.Property.NAME;
@@ -41,7 +40,6 @@
 import org.junit.Test;
 import org.mockito.internal.util.collections.Sets;
 
-import org.apache.qpid.server.security.access.firewall.FirewallRule;
 import org.apache.qpid.server.security.access.firewall.FirewallRuleFactory;
 import org.apache.qpid.test.utils.UnitTestBase;
 
@@ -125,12 +123,10 @@
     {
         _aclRulePredicates.parse(ATTRIBUTES.name(), "attribute1,attribute2");
         _aclRulePredicates.parse(FROM_NETWORK.name(), "network1,network2");
-        _aclRulePredicates.parse(CONNECTION_LIMIT.name(), "20");
 
         final Map<ObjectProperties.Property, String> properties = _aclRulePredicates.getParsedProperties();
 
         assertThat(properties, allOf(hasEntry(ATTRIBUTES, "attribute1,attribute2"),
-                                     hasEntry(FROM_NETWORK, "network1,network2"),
-                                     hasEntry(CONNECTION_LIMIT, "20")));
+                                     hasEntry(FROM_NETWORK, "network1,network2")));
     }
 }
diff --git a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/ClientActionTest.java b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/ClientActionTest.java
index bb5479c..aa1c425 100644
--- a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/ClientActionTest.java
+++ b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/ClientActionTest.java
@@ -22,10 +22,8 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
-import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.security.Principal;
 import java.util.Collections;
@@ -37,7 +35,6 @@
 import org.junit.Test;
 
 import org.apache.qpid.server.connection.ConnectionPrincipal;
-import org.apache.qpid.server.security.access.firewall.FirewallRule;
 import org.apache.qpid.server.transport.AMQPConnection;
 import org.apache.qpid.server.virtualhost.QueueManagingVirtualHost;
 import org.apache.qpid.test.utils.UnitTestBase;
@@ -67,7 +64,7 @@
         final Set<? extends Principal> principals = Collections.singleton(connectionPrincipal);
         _subject = new Subject(false, principals, Collections.emptySet(), Collections.emptySet());
         _ruleAction = mock(AclAction.class);
-        when(_ruleAction.getDynamicRule()).thenReturn(subject -> true);
+        when(_ruleAction.getFirewallRule()).thenReturn(subject -> true);
         _action = mock(Action.class);
         _clientAction = new ClientAction(_action);
     }
@@ -76,7 +73,7 @@
     public void testMatches_returnsTrueWhenActionsMatchAndNoFirewallRule()
     {
         when(_action.matches(any(Action.class))).thenReturn(true);
-        when(_ruleAction.getDynamicRule()).thenReturn(null);
+        when(_ruleAction.getFirewallRule()).thenReturn(null);
         when(_ruleAction.getAction()).thenReturn(mock(Action.class));
 
         assertTrue(_clientAction.matches(_ruleAction, _subject));
@@ -87,10 +84,10 @@
     {
         FirewallRule firewallRule = mock(FirewallRule.class);
         when(firewallRule.matches(_subject)).thenReturn(true);
-        when(_ruleAction.getDynamicRule()).thenReturn(firewallRule);
+        when(_ruleAction.getFirewallRule()).thenReturn(firewallRule);
 
         when(_action.matches(any(Action.class))).thenReturn(false);
-        when(_ruleAction.getDynamicRule()).thenReturn(firewallRule);
+        when(_ruleAction.getFirewallRule()).thenReturn(firewallRule);
         when(_ruleAction.getAction()).thenReturn(mock(Action.class));
 
         assertFalse(_clientAction.matches(_ruleAction, _subject));
@@ -101,45 +98,31 @@
     {
         FirewallRule firewallRule = mock(FirewallRule.class);
         when(firewallRule.matches(_subject)).thenReturn(true);
-        when(_ruleAction.getDynamicRule()).thenReturn(firewallRule);
+        when(_ruleAction.getFirewallRule()).thenReturn(firewallRule);
 
         when(_action.matches(any(Action.class))).thenReturn(true);
-        when(_ruleAction.getDynamicRule()).thenReturn(firewallRule);
+        when(_ruleAction.getFirewallRule()).thenReturn(firewallRule);
         when(_ruleAction.getAction()).thenReturn(mock(Action.class));
 
         assertTrue(_clientAction.matches(_ruleAction, _subject));
     }
 
     @Test
-    public void testMatches_ignoresFirewallRuleIfClientAddressIsNull()
+    public void testMatches_firewallRule()
     {
         FirewallRule firewallRule = new FirewallRule()
         {
             @Override
-            protected boolean matches(final InetAddress addressOfClient)
+            public boolean matches(Subject subject)
             {
-                return false;
+                return true;
             }
         };
 
         when(_action.matches(any(Action.class))).thenReturn(true);
-        when(_ruleAction.getDynamicRule()).thenReturn(firewallRule);
+        when(_ruleAction.getFirewallRule()).thenReturn(firewallRule);
         when(_ruleAction.getAction()).thenReturn(mock(Action.class));
 
         assertTrue(_clientAction.matches(_ruleAction, _subject));
     }
-
-    @Test
-    public void testMatchesWhenConnectionLimitBreached()
-    {
-        final ObjectProperties properties = new ObjectProperties("foo");
-        final AclRulePredicates predicates = new AclRulePredicates();
-        predicates.parse(ObjectProperties.Property.CONNECTION_LIMIT.name(), "1");
-        final AclAction ruleAction = new AclAction(LegacyOperation.ACCESS, ObjectType.VIRTUALHOST, predicates);
-        final ClientAction clientAction = new ClientAction(LegacyOperation.ACCESS, ObjectType.VIRTUALHOST, properties);
-
-        when(_connection.getAuthenticatedPrincipalConnectionCount()).thenReturn(2);
-        boolean matches = clientAction.matches(ruleAction, _subject);
-        assertFalse(matches);
-    }
 }
diff --git a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/DynamicRuleTest.java b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/FirewallRuleTest.java
similarity index 77%
rename from broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/DynamicRuleTest.java
rename to broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/FirewallRuleTest.java
index 9b533bb..1e32e5f 100644
--- a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/DynamicRuleTest.java
+++ b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/FirewallRuleTest.java
@@ -28,17 +28,17 @@
 
 import org.apache.qpid.test.utils.UnitTestBase;
 
-public class DynamicRuleTest extends UnitTestBase
+public class FirewallRuleTest extends UnitTestBase
 {
 
     @Test
     public void subjectMatchesAllRules()
     {
         final Subject subject = new Subject();
-        final DynamicRule rule1 = s -> s.equals(subject);
-        final DynamicRule rule2 = s -> s.equals(subject);
+        final FirewallRule rule1 = s -> s.equals(subject);
+        final FirewallRule rule2 = s -> s.equals(subject);
 
-        final DynamicRule combined = rule1.and(rule2);
+        final FirewallRule combined = rule1.and(rule2);
 
         assertThat(combined.matches(subject), is(true));
     }
@@ -47,10 +47,10 @@
     public void subjectDoesNotMatchAllRules()
     {
         final Subject subject = new Subject();
-        final DynamicRule rule1 = s -> s.equals(subject);
-        final DynamicRule rule2 = s -> !s.equals(subject);
+        final FirewallRule rule1 = s -> s.equals(subject);
+        final FirewallRule rule2 = s -> !s.equals(subject);
 
-        final DynamicRule combined = rule1.and(rule2);
+        final FirewallRule combined = rule1.and(rule2);
 
         assertThat(combined.matches(subject), is(false));
     }
diff --git a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalFrequencyLimitRuleTest.java b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalFrequencyLimitRuleTest.java
deleted file mode 100644
index 1619e72..0000000
--- a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalFrequencyLimitRuleTest.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.qpid.server.security.access.config.connection;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.util.Collections;
-
-import javax.security.auth.Subject;
-
-import org.junit.Before;
-import org.junit.Test;
-
-import org.apache.qpid.server.connection.ConnectionPrincipal;
-import org.apache.qpid.server.transport.AMQPConnection;
-import org.apache.qpid.test.utils.UnitTestBase;
-
-public class ConnectionPrincipalFrequencyLimitRuleTest extends UnitTestBase
-{
-
-    private AMQPConnection _connection;
-    private Subject _subject;
-
-    @Before
-    public void setUp()
-    {
-        _connection = mock(AMQPConnection.class);
-
-        final ConnectionPrincipal connectionPrincipal = mock(ConnectionPrincipal.class);
-        when(connectionPrincipal.getConnection()).thenReturn(_connection);
-        when(_connection.getAuthorizedPrincipal()).thenReturn(connectionPrincipal);
-        _subject = new Subject(false,
-                               Collections.singleton(connectionPrincipal),
-                               Collections.emptySet(),
-                               Collections.emptySet());
-    }
-
-    @Test
-    public void limitBreached()
-    {
-        when(_connection.getAuthenticatedPrincipalConnectionFrequency()).thenReturn(2);
-        final ConnectionPrincipalFrequencyLimitRule rule = new ConnectionPrincipalFrequencyLimitRule(1);
-
-        assertThat(rule.matches(_subject), is(false));
-    }
-
-    @Test
-    public void frequencyEqualsLimit()
-    {
-        when(_connection.getAuthenticatedPrincipalConnectionFrequency()).thenReturn(2);
-        final ConnectionPrincipalFrequencyLimitRule rule = new ConnectionPrincipalFrequencyLimitRule(2);
-
-        assertThat(rule.matches(_subject), is(true));
-    }
-
-    @Test
-    public void frequencyBelowLimit()
-    {
-        when(_connection.getAuthenticatedPrincipalConnectionFrequency()).thenReturn(1);
-        final ConnectionPrincipalFrequencyLimitRule rule = new ConnectionPrincipalFrequencyLimitRule(2);
-
-        assertThat(rule.matches(_subject), is(true));
-    }
-}
diff --git a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalLimitRuleTest.java b/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalLimitRuleTest.java
deleted file mode 100644
index 4b5598a..0000000
--- a/broker-plugins/access-control/src/test/java/org/apache/qpid/server/security/access/config/connection/ConnectionPrincipalLimitRuleTest.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.qpid.server.security.access.config.connection;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-import java.util.Collections;
-
-import javax.security.auth.Subject;
-
-import org.junit.Before;
-import org.junit.Test;
-
-import org.apache.qpid.server.connection.ConnectionPrincipal;
-import org.apache.qpid.server.transport.AMQPConnection;
-import org.apache.qpid.test.utils.UnitTestBase;
-
-public class ConnectionPrincipalLimitRuleTest extends UnitTestBase
-{
-
-    private AMQPConnection _connection;
-    private Subject _subject;
-
-    @Before
-    public void setUp()
-    {
-        _connection = mock(AMQPConnection.class);
-
-        final ConnectionPrincipal connectionPrincipal = mock(ConnectionPrincipal.class);
-        when(connectionPrincipal.getConnection()).thenReturn(_connection);
-        when(_connection.getAuthorizedPrincipal()).thenReturn(connectionPrincipal);
-        _subject = new Subject(false,
-                               Collections.singleton(connectionPrincipal),
-                               Collections.emptySet(),
-                               Collections.emptySet());
-    }
-
-    @Test
-    public void limitBreached()
-    {
-        when(_connection.getAuthenticatedPrincipalConnectionCount()).thenReturn(2);
-        final ConnectionPrincipalLimitRule rule = new ConnectionPrincipalLimitRule(1);
-
-        assertThat(rule.matches(_subject), is(false));
-    }
-
-    @Test
-    public void frequencyEqualsLimit()
-    {
-        when(_connection.getAuthenticatedPrincipalConnectionCount()).thenReturn(2);
-        final ConnectionPrincipalLimitRule rule = new ConnectionPrincipalLimitRule(2);
-
-        assertThat(rule.matches(_subject), is(true));
-    }
-
-    @Test
-    public void frequencyBelowLimit()
-    {
-        when(_connection.getAuthenticatedPrincipalConnectionCount()).thenReturn(1);
-        final ConnectionPrincipalLimitRule rule = new ConnectionPrincipalLimitRule(2);
-
-        assertThat(rule.matches(_subject), is(true));
-    }
-}
diff --git a/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/AMQPConnection_0_10Impl.java b/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/AMQPConnection_0_10Impl.java
index 1315258..f8c5d29 100755
--- a/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/AMQPConnection_0_10Impl.java
+++ b/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/AMQPConnection_0_10Impl.java
@@ -43,7 +43,6 @@
 import org.apache.qpid.server.model.port.AmqpPort;
 import org.apache.qpid.server.protocol.v0_10.transport.ConnectionCloseCode;
 import org.apache.qpid.server.session.AMQPSession;
-import org.apache.qpid.server.store.StoreException;
 import org.apache.qpid.server.transport.AbstractAMQPConnection;
 import org.apache.qpid.server.transport.AggregateTicker;
 import org.apache.qpid.server.transport.ByteBufferSender;
@@ -53,8 +52,6 @@
 import org.apache.qpid.server.txn.ServerTransaction;
 import org.apache.qpid.server.util.Action;
 import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
-import org.apache.qpid.server.util.ServerScopedRuntimeException;
-
 
 public class AMQPConnection_0_10Impl extends AbstractAMQPConnection<AMQPConnection_0_10Impl, ServerConnection>
         implements
diff --git a/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/ServerConnectionDelegate.java b/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/ServerConnectionDelegate.java
index ef5f8ce..41b42eb 100644
--- a/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/ServerConnectionDelegate.java
+++ b/broker-plugins/amqp-0-10-protocol/src/main/java/org/apache/qpid/server/protocol/v0_10/ServerConnectionDelegate.java
@@ -50,6 +50,7 @@
 import org.apache.qpid.server.security.auth.SubjectAuthenticationResult;
 import org.apache.qpid.server.security.auth.sasl.SaslNegotiator;
 import org.apache.qpid.server.security.auth.sasl.SaslSettings;
+import org.apache.qpid.server.security.limit.ConnectionLimitException;
 import org.apache.qpid.server.transport.AMQPConnection;
 import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
 import org.apache.qpid.server.virtualhost.VirtualHostUnavailableException;
@@ -269,8 +270,10 @@
 
             try
             {
+                final AMQPConnection_0_10 amqpConnection = sconn.getAmqpConnection();
+
                 sconn.setVirtualHost(addressSpace);
-                if(!addressSpace.authoriseCreateConnection(sconn.getAmqpConnection()))
+                if(!addressSpace.authoriseCreateConnection(amqpConnection))
                 {
                     sconn.setState(ServerConnection.State.CLOSING);
                     sconn.sendConnectionClose(ConnectionCloseCode.CONNECTION_FORCED, "Connection not authorized");
@@ -283,6 +286,12 @@
                 sconn.sendConnectionClose(ConnectionCloseCode.CONNECTION_FORCED, e.getMessage());
                 return;
             }
+            catch (ConnectionLimitException e)
+            {
+                LOGGER.debug("User connection limit exceeded", e);
+                sconn.setState(ServerConnection.State.CLOSING);
+                sconn.sendConnectionClose(ConnectionCloseCode.CONNECTION_FORCED, e.getMessage());
+            }
 
             sconn.setState(ServerConnection.State.OPEN);
             _state = ConnectionState.OPEN;
diff --git a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java
index d16eaa0..26aaf95 100644
--- a/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java
+++ b/broker-plugins/amqp-0-8-protocol/src/main/java/org/apache/qpid/server/protocol/v0_8/AMQPConnection_0_8Impl.java
@@ -45,6 +45,7 @@
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.regex.Pattern;
 
+import org.apache.qpid.server.security.limit.ConnectionLimitException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -166,7 +167,6 @@
     private volatile String _closeCause;
     private volatile int _closeCauseCode;
 
-
     public AMQPConnection_0_8Impl(Broker<?> broker,
                                   ServerNetworkConnection network,
                                   AmqpPort<?> port,
@@ -654,14 +654,7 @@
             }
             finally
             {
-                performDeleteTasks();
-
-                final NamedAddressSpace virtualHost = getAddressSpace();
-                if (virtualHost != null)
-                {
-                    virtualHost.deregisterConnection(this);
-                }
-
+                clearConnection();
             }
         }
         catch (ConnectionScopedRuntimeException | TransportException e)
@@ -674,6 +667,22 @@
         }
     }
 
+    private void clearConnection()
+    {
+        try
+        {
+            performDeleteTasks();
+        }
+        finally
+        {
+            final NamedAddressSpace virtualHost = getAddressSpace();
+            if (virtualHost != null)
+            {
+                virtualHost.deregisterConnection(this);
+            }
+        }
+    }
+
     @Override
     protected boolean isOrderlyClose()
     {
@@ -937,10 +946,8 @@
                                       AMQShortString capabilities,
                                       boolean insist)
     {
-        if(LOGGER.isDebugEnabled())
-        {
-            LOGGER.debug("RECV ConnectionOpen[" +" virtualHost: " + virtualHostName + " capabilities: " + capabilities + " insist: " + insist + " ]");
-        }
+        LOGGER.debug("RECV ConnectionOpen[virtualHost: {}, capabilities: {}, insist: {}]",
+                virtualHostName, capabilities, insist);
 
         assertState(ConnectionState.AWAIT_OPEN);
 
@@ -950,56 +957,57 @@
             virtualHostStr = virtualHostStr.substring(1);
         }
 
-        NamedAddressSpace addressSpace = ((AmqpPort)getPort()).getAddressSpace(virtualHostStr);
+        final NamedAddressSpace addressSpace = ((AmqpPort)getPort()).getAddressSpace(virtualHostStr);
 
         if (addressSpace == null)
         {
             sendConnectionClose(ErrorCodes.NOT_FOUND,
                     "Unknown virtual host: '" + virtualHostName + "'", 0);
-
+            return;
         }
-        else
-        {
-            // Check virtualhost access
-            if (!addressSpace.isActive())
-            {
-                String redirectHost = addressSpace.getRedirectHost(getPort());
-                if(redirectHost != null)
-                {
-                    sendConnectionClose(0, new AMQFrame(0, new ConnectionRedirectBody(getProtocolVersion(), AMQShortString.valueOf(redirectHost), null)));
-                }
-                else
-                {
-                    sendConnectionClose(ErrorCodes.CONNECTION_FORCED,
-                            "Virtual host '" + addressSpace.getName() + "' is not active", 0);
-                }
 
+        // Check virtualhost access
+        if (!addressSpace.isActive())
+        {
+            final String redirectHost = addressSpace.getRedirectHost(getPort());
+            if (redirectHost != null)
+            {
+                sendConnectionClose(0, new AMQFrame(0, new ConnectionRedirectBody(getProtocolVersion(), AMQShortString.valueOf(redirectHost), null)));
             }
             else
             {
-                try
-                {
-                    addressSpace.registerConnection(this, new NoopConnectionEstablishmentPolicy());
-                    setAddressSpace(addressSpace);
-
-                    if(addressSpace.authoriseCreateConnection(this))
-                    {
-                        MethodRegistry methodRegistry = getMethodRegistry();
-                        AMQMethodBody responseBody = methodRegistry.createConnectionOpenOkBody(virtualHostName);
-
-                        writeFrame(responseBody.generateFrame(0));
-                        _state = ConnectionState.OPEN;
-                    }
-                    else
-                    {
-                        sendConnectionClose(ErrorCodes.ACCESS_REFUSED, "Connection refused", 0);
-                    }
-                }
-                catch (AccessControlException | VirtualHostUnavailableException e)
-                {
-                    sendConnectionClose(ErrorCodes.ACCESS_REFUSED, e.getMessage(), 0);
-                }
+                sendConnectionClose(ErrorCodes.CONNECTION_FORCED,
+                        "Virtual host '" + addressSpace.getName() + "' is not active", 0);
             }
+            return;
+        }
+
+        try
+        {
+            addressSpace.registerConnection(this, new NoopConnectionEstablishmentPolicy());
+            setAddressSpace(addressSpace);
+
+            if (addressSpace.authoriseCreateConnection(this))
+            {
+                final MethodRegistry methodRegistry = getMethodRegistry();
+                final AMQMethodBody responseBody = methodRegistry.createConnectionOpenOkBody(virtualHostName);
+
+                writeFrame(responseBody.generateFrame(0));
+                _state = ConnectionState.OPEN;
+            }
+            else
+            {
+                sendConnectionClose(ErrorCodes.ACCESS_REFUSED, "Connection refused", 0);
+            }
+        }
+        catch (AccessControlException | VirtualHostUnavailableException e)
+        {
+            sendConnectionClose(ErrorCodes.ACCESS_REFUSED, e.getMessage(), 0);
+        }
+        catch (ConnectionLimitException e)
+        {
+            LOGGER.debug("User connection limit exceeded", e);
+            sendConnectionClose(ErrorCodes.RESOURCE_ERROR, e.getMessage(), 0);
         }
     }
 
diff --git a/broker-plugins/amqp-1-0-protocol/src/main/java/org/apache/qpid/server/protocol/v1_0/AMQPConnection_1_0Impl.java b/broker-plugins/amqp-1-0-protocol/src/main/java/org/apache/qpid/server/protocol/v1_0/AMQPConnection_1_0Impl.java
index 1ffd46d..bce5b85 100644
--- a/broker-plugins/amqp-1-0-protocol/src/main/java/org/apache/qpid/server/protocol/v1_0/AMQPConnection_1_0Impl.java
+++ b/broker-plugins/amqp-1-0-protocol/src/main/java/org/apache/qpid/server/protocol/v1_0/AMQPConnection_1_0Impl.java
@@ -55,6 +55,7 @@
 import com.google.common.collect.PeekingIterator;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.ListenableFuture;
+import org.apache.qpid.server.security.limit.ConnectionLimitException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -913,106 +914,110 @@
             final Error err = new Error();
             populateConnectionRedirect(addressSpace, err);
             closeConnection(err);
+            return;
         }
-        else
+
+        final Principal authenticatedPrincipal = getAuthorizedPrincipal();
+        if (authenticatedPrincipal == null)
         {
-            if (AuthenticatedPrincipal.getOptionalAuthenticatedPrincipalFromSubject(getSubject()) == null)
+            closeConnection(AmqpError.NOT_ALLOWED, "Connection has not been authenticated");
+            return;
+        }
+
+        try
+        {
+            boolean registerSucceeded = addressSpace.registerConnection(this, (existingConnections, newConnection) ->
             {
-                closeConnection(AmqpError.NOT_ALLOWED, "Connection has not been authenticated");
-            }
-            else
-            {
-                try
+                boolean proceedWithRegistration = true;
+                if (newConnection instanceof AMQPConnection_1_0Impl && !newConnection.isClosing())
                 {
-                    boolean registerSucceeded = addressSpace.registerConnection(this, (existingConnections, newConnection) ->
+                    List<ListenableFuture<Void>> rescheduleFutures = new ArrayList<>();
+                    for (AMQPConnection<?> existingConnection : StreamSupport.stream(existingConnections.spliterator(), false)
+                            .filter(con -> con instanceof AMQPConnection_1_0)
+                            .filter(con -> !con.isClosing())
+                            .filter(con -> con.getRemoteContainerName().equals(newConnection.getRemoteContainerName()))
+                            .collect(Collectors.toList()))
                     {
-                        boolean proceedWithRegistration = true;
-                        if (newConnection instanceof AMQPConnection_1_0Impl && !newConnection.isClosing())
+                        SoleConnectionEnforcementPolicy soleConnectionEnforcementPolicy = null;
+                        if (((AMQPConnection_1_0Impl) existingConnection)._soleConnectionEnforcementPolicy
+                                != null)
                         {
-                            List<ListenableFuture<Void>> rescheduleFutures = new ArrayList<>();
-                            for (AMQPConnection<?> existingConnection : StreamSupport.stream(existingConnections.spliterator(), false)
-                                    .filter(con -> con instanceof AMQPConnection_1_0)
-                                    .filter(con -> !con.isClosing())
-                                    .filter(con -> con.getRemoteContainerName().equals(newConnection.getRemoteContainerName()))
-                                    .collect(Collectors.toList()))
-                            {
-                                SoleConnectionEnforcementPolicy soleConnectionEnforcementPolicy = null;
-                                if (((AMQPConnection_1_0Impl) existingConnection)._soleConnectionEnforcementPolicy
-                                        != null)
-                                {
-                                    soleConnectionEnforcementPolicy =
-                                            ((AMQPConnection_1_0Impl) existingConnection)._soleConnectionEnforcementPolicy;
-                                }
-                                else if (((AMQPConnection_1_0Impl) newConnection)._soleConnectionEnforcementPolicy != null)
-                                {
-                                    soleConnectionEnforcementPolicy =
-                                            ((AMQPConnection_1_0Impl) newConnection)._soleConnectionEnforcementPolicy;
-                                }
-                                if (SoleConnectionEnforcementPolicy.REFUSE_CONNECTION.equals(soleConnectionEnforcementPolicy))
-                                {
-                                    _properties.put(Symbol.valueOf("amqp:connection-establishment-failed"), true);
-                                    Error error = new Error(AmqpError.INVALID_FIELD,
-                                            String.format(
-                                                    "Connection closed due to sole-connection-enforcement-policy '%s'",
-                                                    soleConnectionEnforcementPolicy.toString()));
-                                    error.setInfo(Collections.singletonMap(Symbol.valueOf("invalid-field"), Symbol.valueOf("container-id")));
-                                    newConnection.doOnIOThreadAsync(() -> ((AMQPConnection_1_0Impl) newConnection).closeConnection(error));
-                                    proceedWithRegistration = false;
-                                    break;
-                                }
-                                else if (SoleConnectionEnforcementPolicy.CLOSE_EXISTING.equals(soleConnectionEnforcementPolicy))
-                                {
-                                    final Error error = new Error(AmqpError.RESOURCE_LOCKED,
-                                            String.format(
-                                                    "Connection closed due to sole-connection-enforcement-policy '%s'",
-                                                    soleConnectionEnforcementPolicy.toString()));
-                                    error.setInfo(Collections.singletonMap(Symbol.valueOf("sole-connection-enforcement"), true));
-                                    rescheduleFutures.add(existingConnection.doOnIOThreadAsync(
-                                            () -> ((AMQPConnection_1_0Impl) existingConnection).closeConnection(error)));
-                                    proceedWithRegistration = false;
-                                }
-                            }
-                            if (!rescheduleFutures.isEmpty())
-                            {
-                                doAfter(allAsList(rescheduleFutures), () -> newConnection.doOnIOThreadAsync(() -> receiveOpenInternal(addressSpace)));
-                            }
+                            soleConnectionEnforcementPolicy =
+                                    ((AMQPConnection_1_0Impl) existingConnection)._soleConnectionEnforcementPolicy;
                         }
-                        return proceedWithRegistration;
-                    });
-
-                    if (registerSucceeded)
-                    {
-                        setAddressSpace(addressSpace);
-
-                        if (!addressSpace.authoriseCreateConnection(this))
+                        else if (((AMQPConnection_1_0Impl) newConnection)._soleConnectionEnforcementPolicy != null)
                         {
-                            closeConnection(AmqpError.NOT_ALLOWED, "Connection refused");
+                            soleConnectionEnforcementPolicy =
+                                    ((AMQPConnection_1_0Impl) newConnection)._soleConnectionEnforcementPolicy;
                         }
-                        else
+                        if (SoleConnectionEnforcementPolicy.REFUSE_CONNECTION.equals(soleConnectionEnforcementPolicy))
                         {
-                            switch (_connectionState)
-                            {
-                                case AWAIT_OPEN:
-                                    sendOpen(_channelMax, _maxFrameSize);
-                                    _connectionState = ConnectionState.OPENED;
-                                    break;
-                                case CLOSE_SENT:
-                                case CLOSED:
-                                    // already sent our close - probably due to an error
-                                    break;
-                                default:
-                                    throw new ConnectionScopedRuntimeException(String.format(
-                                            "Unexpected state %s during connection open.", _connectionState));
-                            }
+                            _properties.put(Symbol.valueOf("amqp:connection-establishment-failed"), true);
+                            Error error = new Error(AmqpError.INVALID_FIELD,
+                                    String.format(
+                                            "Connection closed due to sole-connection-enforcement-policy '%s'",
+                                            soleConnectionEnforcementPolicy.toString()));
+                            error.setInfo(Collections.singletonMap(Symbol.valueOf("invalid-field"), Symbol.valueOf("container-id")));
+                            newConnection.doOnIOThreadAsync(() -> ((AMQPConnection_1_0Impl) newConnection).closeConnection(error));
+                            proceedWithRegistration = false;
+                            break;
+                        }
+                        else if (SoleConnectionEnforcementPolicy.CLOSE_EXISTING.equals(soleConnectionEnforcementPolicy))
+                        {
+                            final Error error = new Error(AmqpError.RESOURCE_LOCKED,
+                                    String.format(
+                                            "Connection closed due to sole-connection-enforcement-policy '%s'",
+                                            soleConnectionEnforcementPolicy.toString()));
+                            error.setInfo(Collections.singletonMap(Symbol.valueOf("sole-connection-enforcement"), true));
+                            rescheduleFutures.add(existingConnection.doOnIOThreadAsync(
+                                    () -> ((AMQPConnection_1_0Impl) existingConnection).closeConnection(error)));
+                            proceedWithRegistration = false;
                         }
                     }
+                    if (!rescheduleFutures.isEmpty())
+                    {
+                        doAfter(allAsList(rescheduleFutures), () -> newConnection.doOnIOThreadAsync(() -> receiveOpenInternal(addressSpace)));
+                    }
                 }
-                catch (VirtualHostUnavailableException | AccessControlException e)
+                return proceedWithRegistration;
+            });
+
+            if (registerSucceeded)
+            {
+                setAddressSpace(addressSpace);
+
+                if (!addressSpace.authoriseCreateConnection(this))
                 {
-                    closeConnection(AmqpError.NOT_ALLOWED, e.getMessage());
+                    closeConnection(AmqpError.NOT_ALLOWED, "Connection refused");
+                }
+                else
+                {
+                    switch (_connectionState)
+                    {
+                        case AWAIT_OPEN:
+                            sendOpen(_channelMax, _maxFrameSize);
+                            _connectionState = ConnectionState.OPENED;
+                            break;
+                        case CLOSE_SENT:
+                        case CLOSED:
+                            // already sent our close - probably due to an error
+                            break;
+                        default:
+                            throw new ConnectionScopedRuntimeException(String.format(
+                                    "Unexpected state %s during connection open.", _connectionState));
+                    }
                 }
             }
         }
+        catch (VirtualHostUnavailableException | AccessControlException e)
+        {
+            closeConnection(AmqpError.NOT_ALLOWED, e.getMessage());
+        }
+        catch (ConnectionLimitException e)
+        {
+            LOGGER.debug("User connection limit exceeded", e);
+            closeConnection(AmqpError.RESOURCE_LIMIT_EXCEEDED, e.getMessage());
+        }
     }
 
     private void populateConnectionRedirect(final NamedAddressSpace addressSpace, final Error err)
@@ -1151,7 +1156,7 @@
         }
         finally
         {
-            NamedAddressSpace virtualHost = getAddressSpace();
+            final NamedAddressSpace virtualHost = getAddressSpace();
             if (virtualHost != null)
             {
                 virtualHost.deregisterConnection(this);
diff --git a/broker-plugins/connection-limits/pom.xml b/broker-plugins/connection-limits/pom.xml
new file mode 100644
index 0000000..da4708b
--- /dev/null
+++ b/broker-plugins/connection-limits/pom.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+  
+  http://www.apache.org/licenses/LICENSE-2.0
+  
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.qpid</groupId>
+    <artifactId>qpid-broker-parent</artifactId>
+    <version>9.0.0-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>qpid-broker-plugins-connection-limits</artifactId>
+  <name>Apache Qpid Broker-J Connection Limit Plug-in</name>
+  <description>Connection Limit broker plug-in</description>
+
+  <properties>
+    <generated-logmessages-dir>${project.build.directory}/generated-sources/generated-logmessages</generated-logmessages-dir>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>qpid-broker-core</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>qpid-broker-codegen</artifactId>
+      <optional>true</optional>
+    </dependency>
+
+    <!-- test dependencies -->
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>qpid-test-utils</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
+      <artifactId>qpid-broker-core</artifactId>
+      <classifier>tests</classifier>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <resources>
+      <resource>
+        <directory>src/main/java</directory>
+        <excludes>
+          <exclude>**/*.java</exclude>
+        </excludes>
+      </resource>
+      <resource>
+        <directory>src/main/resources</directory>
+        <includes>
+          <include>META-INF/</include>
+        </includes>
+      </resource>
+    </resources>
+  </build>
+
+</project>
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/AbstractRule.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/AbstractRule.java
new file mode 100644
index 0000000..81b0c52
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/AbstractRule.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.util.Optional;
+
+abstract class AbstractRule implements Rule
+{
+    private final String _port;
+    private final String _identity;
+
+    AbstractRule(String port, String identity)
+    {
+        super();
+        this._port = Optional.ofNullable(port).orElse(RulePredicates.ALL_PORTS);
+        this._identity = Optional.ofNullable(identity).orElse(RulePredicates.ALL_USERS);
+    }
+
+    @Override
+    public String getPort()
+    {
+        return _port;
+    }
+
+    @Override
+    public String getIdentity()
+    {
+        return _identity;
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/BlockingRule.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/BlockingRule.java
new file mode 100644
index 0000000..27ca9d1
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/BlockingRule.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.qpid.server.user.connection.limits.plugins.ConnectionLimitRule;
+
+final class BlockingRule extends AbstractRule
+{
+    BlockingRule(ConnectionLimitRule rule)
+    {
+        this(rule.getPort(), rule.getIdentity());
+    }
+
+    BlockingRule(String port, String identity)
+    {
+        super(port, identity);
+    }
+
+    @Override
+    public boolean isUserBlocked()
+    {
+        return true;
+    }
+
+    @Override
+    public Integer getCountLimit()
+    {
+        return 0;
+    }
+
+    @Override
+    public Integer getFrequencyLimit()
+    {
+        return 0;
+    }
+
+    @Override
+    public Duration getFrequencyPeriod()
+    {
+        return Duration.ofMinutes(1L);
+    }
+
+    @Override
+    public Map<Duration, Integer> getFrequencyLimits()
+    {
+        return Collections.emptyMap();
+    }
+
+    @Override
+    public void updateWithDefaultFrequencyPeriod(Duration period)
+    {
+        // Do nothing
+    }
+
+    @Override
+    public boolean isEmpty()
+    {
+        return false;
+    }
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/CombinableLimit.java
similarity index 79%
copy from broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
copy to broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/CombinableLimit.java
index 661b89b..c4b57fa 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/CombinableLimit.java
@@ -16,12 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.qpid.server.user.connection.limits.config;
 
-package org.apache.qpid.server.virtualhost;
-
-public interface ConnectionPrincipalStatistics
+public interface CombinableLimit<T extends CombinableLimit<T>>
 {
-    int getConnectionCount();
+    boolean isEmpty();
 
-    int getConnectionFrequency();
+    T then(T other);
+
+    T mergeWith(T second);
 }
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/ConnectionCountLimit.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/ConnectionCountLimit.java
new file mode 100644
index 0000000..47e7dd5
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/ConnectionCountLimit.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.util.Optional;
+
+@FunctionalInterface
+public interface ConnectionCountLimit extends CombinableLimit<ConnectionCountLimit>
+{
+    Integer getCountLimit();
+
+    default boolean isUserBlocked()
+    {
+        return false;
+    }
+
+    @Override
+    default boolean isEmpty()
+    {
+        return getCountLimit() == null;
+    }
+
+    @Override
+    default ConnectionCountLimit then(ConnectionCountLimit other)
+    {
+        if (other != null && isEmpty())
+        {
+            return other;
+        }
+        return this;
+    }
+
+    @Override
+    default ConnectionCountLimit mergeWith(ConnectionCountLimit second)
+    {
+        if (second == null || isUserBlocked())
+        {
+            return this;
+        }
+        if (second.isUserBlocked())
+        {
+            return second;
+        }
+        final Integer counter = ConnectionLimitsImpl.min(getCountLimit(), second.getCountLimit());
+        return () -> counter;
+    }
+
+    static ConnectionCountLimit newInstance(Rule rule)
+    {
+        if (rule.isUserBlocked())
+        {
+            return blockedUser();
+        }
+        if (rule.getCountLimit() == null)
+        {
+            return noLimits();
+        }
+        final Integer counterLimit = rule.getCountLimit();
+        return () -> counterLimit;
+    }
+
+    static ConnectionCountLimit noLimits()
+    {
+        return NoLimits.INSTANCE;
+    }
+
+    static ConnectionCountLimit blockedUser()
+    {
+        return Blocked.INSTANCE;
+    }
+
+    final class NoLimits implements ConnectionCountLimit
+    {
+        static final NoLimits INSTANCE = new NoLimits();
+
+        private NoLimits()
+        {
+            super();
+        }
+
+        @Override
+        public Integer getCountLimit()
+        {
+            return null;
+        }
+
+        @Override
+        public boolean isUserBlocked()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isEmpty()
+        {
+            return true;
+        }
+
+        @Override
+        public ConnectionCountLimit mergeWith(ConnectionCountLimit second)
+        {
+            return Optional.ofNullable(second).orElse(this);
+        }
+
+        @Override
+        public ConnectionCountLimit then(ConnectionCountLimit other)
+        {
+            return Optional.ofNullable(other).orElse(this);
+        }
+    }
+
+    final class Blocked implements ConnectionCountLimit
+    {
+        static final Blocked INSTANCE = new Blocked();
+
+        private Blocked()
+        {
+            super();
+        }
+
+        @Override
+        public Integer getCountLimit()
+        {
+            return 0;
+        }
+
+        @Override
+        public boolean isEmpty()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isUserBlocked()
+        {
+            return true;
+        }
+
+        @Override
+        public ConnectionCountLimit then(ConnectionCountLimit other)
+        {
+            return this;
+        }
+
+        @Override
+        public ConnectionCountLimit mergeWith(ConnectionCountLimit second)
+        {
+            return this;
+        }
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/ConnectionLimits.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/ConnectionLimits.java
new file mode 100644
index 0000000..a880f01
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/ConnectionLimits.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+
+public interface ConnectionLimits extends CombinableLimit<ConnectionLimits>
+{
+    Integer getCountLimit();
+
+    Map<Duration, Integer> getFrequencyLimits();
+
+    boolean isUserBlocked();
+
+    @Override
+    default ConnectionLimits then(ConnectionLimits other)
+    {
+        if (other != null && isEmpty())
+        {
+            return other;
+        }
+        return this;
+    }
+
+    @Override
+    default ConnectionLimits mergeWith(ConnectionLimits second)
+    {
+        if (second == null || isUserBlocked() || second.isEmpty())
+        {
+            return this;
+        }
+        if (second.isUserBlocked() || isEmpty())
+        {
+            return second;
+        }
+        return new ConnectionLimitsImpl(this, second);
+    }
+
+    static ConnectionLimits noLimits()
+    {
+        return NoLimits.INSTANCE;
+    }
+
+    final class NoLimits implements ConnectionLimits
+    {
+        static final NoLimits INSTANCE = new NoLimits();
+
+        private NoLimits()
+        {
+            super();
+        }
+
+        @Override
+        public Integer getCountLimit()
+        {
+            return null;
+        }
+
+        @Override
+        public Map<Duration, Integer> getFrequencyLimits()
+        {
+            return Collections.emptyMap();
+        }
+
+        @Override
+        public boolean isUserBlocked()
+        {
+            return false;
+        }
+
+        @Override
+        public boolean isEmpty()
+        {
+            return true;
+        }
+
+        @Override
+        public ConnectionLimits mergeWith(ConnectionLimits second)
+        {
+            return Optional.ofNullable(second).orElse(this);
+        }
+
+        @Override
+        public ConnectionLimits then(ConnectionLimits other)
+        {
+            return Optional.ofNullable(other).orElse(this);
+        }
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/ConnectionLimitsImpl.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/ConnectionLimitsImpl.java
new file mode 100644
index 0000000..92cf010
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/ConnectionLimitsImpl.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+final class ConnectionLimitsImpl implements ConnectionLimits
+{
+    private final Integer _connectionCount;
+
+    private final Map<Duration, Integer> _connectionFrequency;
+
+    ConnectionLimitsImpl(final ConnectionLimits first, final ConnectionLimits second)
+    {
+        super();
+        _connectionCount = min(first.getCountLimit(), second.getCountLimit());
+        _connectionFrequency = new HashMap<>(first.getFrequencyLimits());
+        second.getFrequencyLimits().forEach(
+                (duration, limit) -> _connectionFrequency.merge(duration, limit, ConnectionLimitsImpl::min));
+        if (_connectionCount == null && _connectionFrequency.isEmpty())
+        {
+            throw new IllegalArgumentException("Unexpected empty limits");
+        }
+    }
+
+    @Override
+    public boolean isEmpty()
+    {
+        return false;
+    }
+
+    @Override
+    public Integer getCountLimit()
+    {
+        return _connectionCount;
+    }
+
+    @Override
+    public Map<Duration, Integer> getFrequencyLimits()
+    {
+        return Collections.unmodifiableMap(_connectionFrequency);
+    }
+
+    @Override
+    public boolean isUserBlocked()
+    {
+        return false;
+    }
+
+    static Integer min(final Integer first, final Integer second)
+    {
+        if (first == null)
+        {
+            return second;
+        }
+        if (second == null)
+        {
+            return first;
+        }
+        return Math.min(first, second);
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/FileParser.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/FileParser.java
new file mode 100644
index 0000000..326d48c
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/FileParser.java
@@ -0,0 +1,346 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StreamTokenizer;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Paths;
+import java.util.ArrayDeque;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Queue;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.qpid.server.configuration.IllegalConfigurationException;
+
+public class FileParser
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(FileParser.class);
+
+    private static final Character COMMENT = '#';
+    private static final Character CONTINUATION = '\\';
+
+    private static final Pattern NUMBER = Pattern.compile("\\s*(\\d+)\\s*");
+
+    private static final String ACCESS_CONTROL = "acl";
+    public static final String CONNECTION_LIMIT = "clt";
+    private static final String CONFIG = "config";
+
+    public static final String DEFAULT_FREQUENCY_PERIOD = "default_frequency_period";
+    public static final String DEFAULT_FREQUENCY_PERIOD_ALTERNATIVE = "defaultfrequencyperiod";
+    private static final String LOG_ALL = "log_all";
+    private static final String LOG_ALL_ALTERNATIVE = "logall";
+
+    public static final String BLOCK = "BLOCK";
+
+    static final String UNRECOGNISED_INITIAL_TOKEN = "Unrecognised initial token '%s' at line %d";
+    static final String NOT_ENOUGH_TOKENS = "Not enough tokens at line %d";
+    static final String NUMBER_NOT_ALLOWED = "Number not allowed before '%s' at line %d";
+    static final String CANNOT_LOAD_CONFIGURATION = "I/O Error while reading configuration";
+    static final String PREMATURE_CONTINUATION = "Premature continuation character at line %d";
+    static final String PARSE_TOKEN_FAILED = "Failed to parse token at line %d";
+    static final String UNKNOWN_CLT_PROPERTY_MSG = "Unknown connection limit property: %s at line %d";
+    static final String PROPERTY_KEY_ONLY_MSG = "Incomplete property (key only) at line %d";
+    static final String PROPERTY_NO_EQUALS_MSG = "Incomplete property (no equals) at line %d";
+    static final String PROPERTY_NO_VALUE_MSG = "Incomplete property (no value) at line %d";
+
+    public static RuleSetCreator parse(String name)
+    {
+        return new FileParser().readAndParse(name);
+    }
+
+    public RuleSetCreator readAndParse(String name)
+    {
+        return readAndParse(getReaderFromURLString(name));
+    }
+
+    RuleSetCreator readAndParse(Reader reader)
+    {
+        final RuleSetCreator ruleSetCreator = new RuleSetCreator();
+
+        int line = 0;
+        try (final Reader fileReader = new BufferedReader(reader))
+        {
+            LOGGER.debug("About to load connection limit file");
+            final StreamTokenizer tokenizer = new StreamTokenizer(fileReader);
+            tokenizer.resetSyntax(); // setup the tokenizer
+
+            tokenizer.commentChar(COMMENT); // single line comments
+            tokenizer.eolIsSignificant(true); // return EOL as a token
+            tokenizer.ordinaryChar('='); // equals is a token
+            tokenizer.ordinaryChar(CONTINUATION); // continuation character (when followed by EOL)
+            tokenizer.quoteChar('"'); // double quote
+            tokenizer.quoteChar('\''); // single quote
+            tokenizer.whitespaceChars('\u0000', '\u0020');
+            tokenizer.wordChars('a', 'z'); // unquoted token characters [a-z]
+            tokenizer.wordChars('A', 'Z'); // [A-Z]
+            tokenizer.wordChars('0', '9'); // [0-9]
+            tokenizer.wordChars('_', '_'); // underscore
+            tokenizer.wordChars('-', '-'); // dash
+            tokenizer.wordChars('.', '.'); // dot
+            tokenizer.wordChars('*', '*'); // star
+            tokenizer.wordChars('@', '@'); // at
+            tokenizer.wordChars(':', ':'); // colon
+            tokenizer.wordChars('+', '+'); // plus
+            tokenizer.wordChars('/', '/');
+
+            final Queue<String> stack = new ArrayDeque<>();
+            int current;
+            do
+            {
+                current = tokenizer.nextToken();
+                line = tokenizer.lineno() - 1;
+                switch (current)
+                {
+                    case StreamTokenizer.TT_EOF:
+                    case StreamTokenizer.TT_EOL:
+                        processLine(ruleSetCreator, line, stack);
+                        break;
+                    case StreamTokenizer.TT_WORD:
+                        addLast(stack, tokenizer.sval);
+                        break;
+                    default:
+                        parseToken(tokenizer, stack);
+                }
+            }
+            while (current != StreamTokenizer.TT_EOF);
+        }
+        catch (IllegalConfigurationException ice)
+        {
+            throw ice;
+        }
+        catch (IOException ioe)
+        {
+            throw new IllegalConfigurationException(CANNOT_LOAD_CONFIGURATION, ioe);
+        }
+        catch (RuntimeException re)
+        {
+            throw new IllegalConfigurationException(String.format(PARSE_TOKEN_FAILED, line), re);
+        }
+        return ruleSetCreator;
+    }
+
+    private void processLine(RuleSetCreator ruleSetCreator, int line, Queue<String> stack)
+    {
+        if (stack.isEmpty())
+        {
+            return; // blank line
+        }
+        final String first = stack.poll();
+        if (first == null || stack.isEmpty())
+        {
+            throw new IllegalConfigurationException(String.format(NOT_ENOUGH_TOKENS, line));
+        }
+
+        final Matcher matcher = NUMBER.matcher(first);
+        if (matcher.matches())
+        {
+            final String commandType = stack.poll();
+            if (ACCESS_CONTROL.equalsIgnoreCase(commandType))
+            {
+                parseAccessControl(stack, ruleSetCreator, line);
+                stack.clear();
+                return;
+            }
+            throw new IllegalConfigurationException(String.format(NUMBER_NOT_ALLOWED, commandType, line));
+        }
+
+        if (CONNECTION_LIMIT.equalsIgnoreCase(first))
+        {
+            parseConnectionLimit(stack, ruleSetCreator, line);
+        }
+        else if (CONFIG.equalsIgnoreCase(first))
+        {
+            parseConfig(stack, ruleSetCreator, line);
+        }
+        else if (ACCESS_CONTROL.equalsIgnoreCase(first))
+        {
+            parseAccessControl(stack, ruleSetCreator, line);
+        }
+        else
+        {
+            throw new IllegalConfigurationException(String.format(UNRECOGNISED_INITIAL_TOKEN, first, line));
+        }
+        stack.clear();
+    }
+
+    private void parseToken(StreamTokenizer tokenizer, Queue<String> stack) throws IOException
+    {
+        if (tokenizer.ttype == CONTINUATION)
+        {
+            if (tokenizer.nextToken() != StreamTokenizer.TT_EOL)
+            {
+                // invalid location for continuation character (add one to line because we ate the EOL)
+                throw new IllegalConfigurationException(String.format(PREMATURE_CONTINUATION, tokenizer.lineno()));
+            }
+        }
+        else if (tokenizer.ttype == '\'' || tokenizer.ttype == '"')
+        {
+            addLast(stack, tokenizer.sval);
+        }
+        else if (!Character.isWhitespace(tokenizer.ttype))
+        {
+            addLast(stack, Character.toString((char) tokenizer.ttype));
+        }
+    }
+
+    private void addLast(Queue<String> queue, String value)
+    {
+        if (value != null)
+        {
+            queue.add(value);
+        }
+    }
+
+    private void parseConnectionLimit(Queue<String> args, final RuleSetCreator ruleSetCreator, final int line)
+    {
+        final String identity = args.poll();
+        final RulePredicates predicates = new RulePredicates();
+
+        final Iterator<String> i = args.iterator();
+        while (i.hasNext())
+        {
+            final String key = i.next();
+            if (BLOCK.equalsIgnoreCase(key))
+            {
+                predicates.setBlockedUser();
+            }
+            else
+            {
+                if (predicates.parse(key, readValue(i, line)) == null)
+                {
+                    throw new IllegalConfigurationException(String.format(UNKNOWN_CLT_PROPERTY_MSG, key, line));
+                }
+            }
+        }
+        if (!predicates.isEmpty())
+        {
+            ruleSetCreator.add(Rule.newInstance(identity, predicates));
+        }
+    }
+
+    private void parseAccessControl(Queue<String> args, final RuleSetCreator ruleSetCreator, final int line)
+    {
+        if (args.size() < 4)
+        {
+            return;
+        }
+        // poll outcome
+        args.poll();
+        final String identity = args.poll();
+        // poll operation
+        args.poll();
+        // poll object
+        args.poll();
+
+        final RulePredicates predicates = new RulePredicates();
+        final Iterator<String> i = args.iterator();
+        while (i.hasNext())
+        {
+            predicates.parse(i.next(), readValue(i, line));
+        }
+        if (!predicates.isEmpty())
+        {
+            ruleSetCreator.add(Rule.newInstance(identity, predicates));
+        }
+    }
+
+    private static void parseConfig(final Queue<String> args, final RuleSetCreator ruleSetCreator, final int line)
+    {
+        final Iterator<String> i = args.iterator();
+        while (i.hasNext())
+        {
+            final String key = i.next().replace("-", "_").toLowerCase(Locale.ENGLISH);
+            final String value = readValue(i, line);
+
+            switch (key)
+            {
+                case LOG_ALL:
+                case LOG_ALL_ALTERNATIVE:
+                    ruleSetCreator.setLogAllMessages(Boolean.parseBoolean(value));
+                    break;
+                case DEFAULT_FREQUENCY_PERIOD:
+                case DEFAULT_FREQUENCY_PERIOD_ALTERNATIVE:
+                    ruleSetCreator.setDefaultFrequencyPeriod(Long.parseLong(value));
+                    break;
+                default:
+            }
+        }
+    }
+
+    private static String readValue(Iterator<String> i, int line)
+    {
+        if (!i.hasNext())
+        {
+            throw new IllegalConfigurationException(String.format(PROPERTY_KEY_ONLY_MSG, line));
+        }
+        if (!"=".equals(i.next()))
+        {
+            throw new IllegalConfigurationException(String.format(PROPERTY_NO_EQUALS_MSG, line));
+        }
+        if (!i.hasNext())
+        {
+            throw new IllegalConfigurationException(String.format(PROPERTY_NO_VALUE_MSG, line));
+        }
+        return i.next();
+    }
+
+    private Reader getReaderFromURLString(String urlString)
+    {
+        try
+        {
+            return new InputStreamReader(new URL(urlString).openStream(), StandardCharsets.UTF_8);
+        }
+        catch (MalformedURLException e)
+        {
+            return getReaderFromPath(urlString);
+        }
+        catch (IOException | RuntimeException e)
+        {
+            throw createReaderError(urlString, e);
+        }
+    }
+
+    private Reader getReaderFromPath(String path)
+    {
+        try
+        {
+            return new InputStreamReader(Paths.get(path).toUri().toURL().openStream(), StandardCharsets.UTF_8);
+        }
+        catch (IOException | RuntimeException e)
+        {
+            throw createReaderError(path, e);
+        }
+    }
+
+    private static IllegalConfigurationException createReaderError(String urlString, Exception e)
+    {
+        return new IllegalConfigurationException("Cannot convert " + urlString + " to a readable resource", e);
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/NonBlockingRule.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/NonBlockingRule.java
new file mode 100644
index 0000000..c0df08e
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/NonBlockingRule.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.time.Duration;
+import java.util.Optional;
+
+import org.apache.qpid.server.user.connection.limits.plugins.ConnectionLimitRule;
+
+final class NonBlockingRule extends AbstractRule
+{
+    private final Integer _connectionCount;
+
+    private final Integer _connectionFrequency;
+
+    private Duration _frequencyPeriod;
+
+    NonBlockingRule(ConnectionLimitRule rule)
+    {
+        this(rule.getPort(), rule.getIdentity(), rule.getCountLimit(), rule.getFrequencyLimit(),
+                Optional.ofNullable(rule.getFrequencyPeriod()).map(Duration::ofMillis).orElse(null));
+    }
+
+    NonBlockingRule(String port, String identity, Integer connectionCount,
+                    Integer connectionFrequency, Duration frequencyPeriod)
+    {
+        super(port, identity);
+        if (connectionCount == null && connectionFrequency == null)
+        {
+            throw new IllegalArgumentException("Empty connection limit rule");
+        }
+        this._connectionCount = connectionCount;
+        this._connectionFrequency = connectionFrequency;
+        this._frequencyPeriod = frequencyPeriod;
+    }
+
+    @Override
+    public boolean isEmpty()
+    {
+        return false;
+    }
+
+    @Override
+    public boolean isUserBlocked()
+    {
+        return false;
+    }
+
+    @Override
+    public Integer getCountLimit()
+    {
+        return _connectionCount;
+    }
+
+    @Override
+    public Integer getFrequencyLimit()
+    {
+        return _connectionFrequency;
+    }
+
+    @Override
+    public Duration getFrequencyPeriod()
+    {
+        return _frequencyPeriod;
+    }
+
+    @Override
+    public void updateWithDefaultFrequencyPeriod(Duration period)
+    {
+        if (_frequencyPeriod == null)
+        {
+            _frequencyPeriod = period;
+        }
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/PortConnectionCounter.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/PortConnectionCounter.java
new file mode 100644
index 0000000..205684d
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/PortConnectionCounter.java
@@ -0,0 +1,526 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.security.Principal;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.NavigableSet;
+import java.util.Objects;
+import java.util.Queue;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import javax.security.auth.Subject;
+
+import org.apache.qpid.server.security.group.GroupPrincipal;
+import org.apache.qpid.server.security.limit.ConnectionLimitException;
+import org.apache.qpid.server.security.limit.ConnectionLimiter;
+import org.apache.qpid.server.security.limit.ConnectionSlot;
+import org.apache.qpid.server.transport.AMQPConnection;
+import org.apache.qpid.server.user.connection.limits.outcome.AcceptRegistration;
+import org.apache.qpid.server.user.connection.limits.outcome.RejectRegistration;
+
+final class PortConnectionCounter
+{
+    private final Function<String, ConnectionCounter> _connectionCounterFactory;
+
+    private final Map<String, ConnectionCounter> _connectionCounters = new ConcurrentHashMap<>();
+
+    PortConnectionCounter(AbstractBuilder<?> builder)
+    {
+        super();
+        _connectionCounterFactory = builder.getConnectionCounterFactory();
+    }
+
+    public AcceptRegistration register(AMQPConnection<?> connection, ConnectionLimiter subLimiter)
+    {
+        final Principal principal = connection.getAuthorizedPrincipal();
+        if (principal == null)
+        {
+            throw new ConnectionLimitException("Unauthorized connection is forbidden");
+        }
+        final String userId = principal.getName();
+        return _connectionCounters.computeIfAbsent(userId, _connectionCounterFactory)
+                .registerConnection(
+                        userId,
+                        collectGroupPrincipals(connection.getSubject()),
+                        connection,
+                        subLimiter);
+    }
+
+    private Set<String> collectGroupPrincipals(Subject subject)
+    {
+        if (subject == null)
+        {
+            return Collections.emptySet();
+        }
+        final Set<String> principalNames = new HashSet<>();
+        for (final Principal principal : subject.getPrincipals(GroupPrincipal.class))
+        {
+            principalNames.add(principal.getName());
+        }
+        return principalNames;
+    }
+
+    static Builder newBuilder(Duration defaultFrequencyPeriod)
+    {
+        if (defaultFrequencyPeriod == null || defaultFrequencyPeriod.isNegative())
+        {
+            return new BuilderWithoutFrequencyImpl();
+        }
+        return new BuilderImpl();
+    }
+
+    public interface Builder
+    {
+        Builder add(Rule rule);
+
+        Builder addAll(Collection<? extends Rule> rule);
+
+        PortConnectionCounter build();
+    }
+
+    private interface ConnectionCounter extends ConnectionSlot
+    {
+        AcceptRegistration registerConnection(
+                String userId, Set<String> groups, AMQPConnection<?> connection, ConnectionLimiter subLimiter);
+    }
+
+    abstract static class AbstractBuilder<T extends CombinableLimit<T>> implements Builder
+    {
+        final Map<String, T> _userLimits = new HashMap<>();
+        T _defaultUserLimits;
+
+        abstract T newLimits(Rule rule);
+
+        abstract Function<String, ConnectionCounter> getConnectionCounterFactory();
+
+        AbstractBuilder(T defaultUserLimits)
+        {
+            super();
+            _defaultUserLimits = defaultUserLimits;
+        }
+
+        @Override
+        public Builder add(Rule rule)
+        {
+            addImpl(rule);
+            return this;
+        }
+
+        @Override
+        public Builder addAll(Collection<? extends Rule> rule)
+        {
+            if (rule != null)
+            {
+                rule.forEach(this::addImpl);
+            }
+            return this;
+        }
+
+        @Override
+        public PortConnectionCounter build()
+        {
+            return new PortConnectionCounter(this);
+        }
+
+        private void addImpl(Rule rule)
+        {
+            if (rule == null)
+            {
+                return;
+            }
+            final T newLimits = newLimits(rule);
+            if (newLimits.isEmpty())
+            {
+                return;
+            }
+            final String id = rule.getIdentity();
+            if (RulePredicates.isAllUser(id))
+            {
+                _defaultUserLimits = newLimits.mergeWith(_defaultUserLimits);
+            }
+            else
+            {
+                _userLimits.merge(id, newLimits, CombinableLimit::mergeWith);
+            }
+        }
+    }
+
+    private static final class BuilderImpl extends AbstractBuilder<ConnectionLimits>
+    {
+        BuilderImpl()
+        {
+            super(ConnectionLimits.noLimits());
+        }
+
+        @Override
+        ConnectionLimits newLimits(Rule rule)
+        {
+            if (rule.getFrequencyLimit() != null &&
+                    (rule.getFrequencyPeriod() == null || rule.getFrequencyPeriod().isNegative()))
+            {
+                throw new IllegalArgumentException("Connection frequency limit needs a time period");
+            }
+            return rule;
+        }
+
+        Function<String, ConnectionCounter> getConnectionCounterFactory()
+        {
+            return userId -> new CombinedConnectionCounterImpl(
+                    new LimitCompiler<>(_userLimits, _defaultUserLimits, ConnectionLimits::noLimits));
+        }
+    }
+
+    private static final class BuilderWithoutFrequencyImpl extends AbstractBuilder<ConnectionCountLimit>
+    {
+        BuilderWithoutFrequencyImpl()
+        {
+            super(ConnectionCountLimit.noLimits());
+        }
+
+        @Override
+        ConnectionCountLimit newLimits(Rule rule)
+        {
+            return ConnectionCountLimit.newInstance(rule);
+        }
+
+        @Override
+        Function<String, ConnectionCounter> getConnectionCounterFactory()
+        {
+            return userId -> new ConnectionCounterImpl(
+                    new LimitCompiler<>(_userLimits, _defaultUserLimits, ConnectionCountLimit::noLimits));
+        }
+    }
+
+    private static final class CombinedConnectionCounterImpl implements ConnectionCounter
+    {
+        private final LimitCompiler<ConnectionLimits> _limits;
+
+        private final Queue<Instant> _registrationTime = new LinkedList<>();
+
+        private long _counter = 0L;
+
+        CombinedConnectionCounterImpl(LimitCompiler<ConnectionLimits> limits)
+        {
+            super();
+            _limits = limits;
+        }
+
+        @Override
+        public synchronized void free()
+        {
+            _counter = Math.max(_counter - 1L, 0L);
+        }
+
+        @Override
+        public AcceptRegistration registerConnection(
+                String userId, Set<String> groups, AMQPConnection<?> connection, ConnectionLimiter subLimiter)
+        {
+            final ConnectionLimits limits = _limits.compileLimits(userId, groups);
+            if (limits.isUserBlocked())
+            {
+                throw RejectRegistration.blockedUser(userId, connection.getPort().getName());
+            }
+            final Integer connectionCountLimit = limits.getCountLimit();
+            final Map<Duration, Integer> connectionFrequencyLimit = limits.getFrequencyLimits();
+
+            if (connectionFrequencyLimit.isEmpty())
+            {
+                if (connectionCountLimit == null)
+                {
+                    return noLimits(userId, connection, subLimiter);
+                }
+                return countLimit(connectionCountLimit, userId, connection, subLimiter);
+            }
+            if (connectionCountLimit == null)
+            {
+                return frequencyLimit(connectionFrequencyLimit, userId, connection, subLimiter);
+            }
+            return bothLimits(connectionCountLimit, connectionFrequencyLimit, userId, connection, subLimiter);
+        }
+
+        private synchronized AcceptRegistration noLimits(
+                String id, AMQPConnection<?> connection, ConnectionLimiter subLimiter)
+        {
+            _registrationTime.clear();
+            final AcceptRegistration result = AcceptRegistration.newInstance(
+                    chainTo(subLimiter.register(connection)),
+                    id,
+                    _counter + 1L,
+                    connection.getPort().getName());
+            _counter = _counter + 1L;
+            return result;
+        }
+
+        private synchronized AcceptRegistration countLimit(
+                Integer countLimit, String id, AMQPConnection<?> connection, ConnectionLimiter subLimiter)
+        {
+            _registrationTime.clear();
+            checkCounter(countLimit, id, connection);
+
+            final AcceptRegistration result = AcceptRegistration.newInstance(
+                    chainTo(subLimiter.register(connection)),
+                    id,
+                    _counter + 1L,
+                    connection.getPort().getName());
+            _counter = _counter + 1L;
+            return result;
+        }
+
+        private synchronized AcceptRegistration frequencyLimit(
+                Map<Duration, Integer> frequencyLimit, String id, AMQPConnection<?> connection, ConnectionLimiter subLimiter)
+        {
+            final Instant now = checkFrequencyLimit(frequencyLimit, id, connection);
+
+            final AcceptRegistration result = AcceptRegistration.newInstance(
+                    chainTo(subLimiter.register(connection)),
+                    id,
+                    _counter + 1L,
+                    connection.getPort().getName());
+            _registrationTime.add(now);
+            _counter = _counter + 1L;
+            return result;
+        }
+
+        private synchronized AcceptRegistration bothLimits(
+                Integer countLimit, Map<Duration, Integer> frequencyLimit, String id,
+                AMQPConnection<?> connection, ConnectionLimiter subLimiter)
+        {
+            final Instant now = checkFrequencyLimit(frequencyLimit, id, connection);
+            checkCounter(countLimit, id, connection);
+
+            final AcceptRegistration result = AcceptRegistration.newInstance(
+                    chainTo(subLimiter.register(connection)),
+                    id,
+                    _counter + 1L,
+                    connection.getPort().getName());
+            _registrationTime.add(now);
+            _counter = _counter + 1L;
+            return result;
+        }
+
+        private void checkCounter(Integer countLimit, String id, AMQPConnection<?> connection)
+        {
+            if (_counter >= countLimit)
+            {
+                throw RejectRegistration.breakingConnectionCount(id, countLimit, connection.getPort().getName());
+            }
+        }
+
+        private Instant checkFrequencyLimit(
+                final Map<Duration, Integer> frequencyLimit, String id, AMQPConnection<?> connection)
+        {
+            final Instant now = Instant.now();
+            final NavigableSet<FrequencyPeriod> periods = new TreeSet<>();
+            for (final Entry<Duration, Integer> entry : frequencyLimit.entrySet())
+            {
+                periods.add(new FrequencyPeriod(now, entry));
+            }
+
+            final Iterator<Instant> registrationTimeIterator = _registrationTime.iterator();
+            int counter = _registrationTime.size();
+            boolean obsoleteRegistrationTime = true;
+
+            while (registrationTimeIterator.hasNext() && !periods.isEmpty())
+            {
+                final Instant registrationTime = registrationTimeIterator.next();
+
+                final Iterator<FrequencyPeriod> periodIterator = periods.iterator();
+                FrequencyPeriod period;
+                while (periodIterator.hasNext() &&
+                        (period = periodIterator.next()).isPeriodBeginningBefore(registrationTime))
+                {
+                    if (!period.isCounterUnderLimit(counter))
+                    {
+                        throw RejectRegistration.breakingConnectionFrequency(
+                                id, period.getLimit(), period.getDuration(), connection.getPort().getName());
+                    }
+                    obsoleteRegistrationTime = false;
+                    periodIterator.remove();
+                }
+                counter--;
+                if (obsoleteRegistrationTime)
+                {
+                    registrationTimeIterator.remove();
+                }
+            }
+            return now;
+        }
+    }
+
+    private static final class ConnectionCounterImpl implements ConnectionCounter
+    {
+        private final LimitCompiler<ConnectionCountLimit> _limits;
+
+        private long _counter = 0L;
+
+        ConnectionCounterImpl(LimitCompiler<ConnectionCountLimit> limits)
+        {
+            super();
+            _limits = limits;
+        }
+
+        @Override
+        public synchronized void free()
+        {
+            _counter = Math.max(_counter - 1L, 0L);
+        }
+
+        @Override
+        public AcceptRegistration registerConnection(
+                String userId, Set<String> groups, AMQPConnection<?> connection, ConnectionLimiter subLimiter)
+        {
+            final ConnectionCountLimit limits = _limits.compileLimits(userId, groups);
+            if (limits.isUserBlocked())
+            {
+                throw RejectRegistration.blockedUser(userId, connection.getPort().getName());
+            }
+            if (limits.getCountLimit() == null)
+            {
+                return noLimits(userId, connection, subLimiter);
+            }
+
+            return countLimit(limits.getCountLimit(), userId, connection, subLimiter);
+        }
+
+        private synchronized AcceptRegistration noLimits(
+                String id, AMQPConnection<?> connection, ConnectionLimiter subLimiter)
+        {
+            final AcceptRegistration result = AcceptRegistration.newInstance(
+                    chainTo(subLimiter.register(connection)),
+                    id,
+                    _counter + 1L,
+                    connection.getPort().getName());
+            _counter = _counter + 1L;
+            return result;
+        }
+
+        private synchronized AcceptRegistration countLimit(
+                Integer countLimit, String id, AMQPConnection<?> connection, ConnectionLimiter subLimiter)
+        {
+            if (_counter >= countLimit)
+            {
+                throw RejectRegistration.breakingConnectionCount(id, countLimit, connection.getPort().getName());
+            }
+            final AcceptRegistration result = AcceptRegistration.newInstance(
+                    chainTo(subLimiter.register(connection)),
+                    id,
+                    _counter + 1L,
+                    connection.getPort().getName());
+            _counter = _counter + 1L;
+            return result;
+        }
+    }
+
+    private static final class LimitCompiler<T extends CombinableLimit<T>>
+    {
+        private final Map<String, T> _limitMap;
+        private final T _defaultLimits;
+        private final Supplier<T> _noLimits;
+
+        LimitCompiler(Map<String, T> limitMap, T defaultLimits, Supplier<T> noLimits)
+        {
+            _limitMap = new HashMap<>(limitMap);
+            _defaultLimits = Objects.requireNonNull(defaultLimits);
+            _noLimits = Objects.requireNonNull(noLimits);
+        }
+
+        public T compileLimits(String userId, Set<String> groups)
+        {
+            final T userLimits = _noLimits.get().mergeWith(_limitMap.get(userId));
+
+            T groupLimits = _noLimits.get();
+            for (final String group : groups)
+            {
+                groupLimits = groupLimits.mergeWith(_limitMap.get(group));
+            }
+
+            return userLimits.then(groupLimits).then(_defaultLimits);
+        }
+    }
+
+    private static final class FrequencyPeriod implements Comparable<FrequencyPeriod>
+    {
+        private final Instant _startTime;
+        private final Duration _duration;
+        private final int _limit;
+
+        FrequencyPeriod(Instant now, Map.Entry<Duration, Integer> limit)
+        {
+            _startTime = now.minus(limit.getKey());
+            _duration = limit.getKey();
+            _limit = limit.getValue();
+        }
+
+        @Override
+        public int compareTo(FrequencyPeriod o)
+        {
+            return _startTime.compareTo(o._startTime);
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return _startTime.hashCode();
+        }
+
+        @Override
+        public boolean equals(Object o)
+        {
+            if (o instanceof FrequencyPeriod)
+            {
+                return _startTime.equals(((FrequencyPeriod) o)._startTime);
+            }
+            return false;
+        }
+
+        boolean isPeriodBeginningBefore(Instant time)
+        {
+            return _startTime.isBefore(time);
+        }
+
+        boolean isCounterUnderLimit(int counter)
+        {
+            return counter < _limit;
+        }
+
+        Duration getDuration()
+        {
+            return _duration;
+        }
+
+        int getLimit()
+        {
+            return _limit;
+        }
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/Rule.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/Rule.java
new file mode 100644
index 0000000..1c63319
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/Rule.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.qpid.server.user.connection.limits.plugins.ConnectionLimitRule;
+
+public interface Rule extends ConnectionLimits
+{
+    String getPort();
+
+    String getIdentity();
+
+    @Override
+    Integer getCountLimit();
+
+    Integer getFrequencyLimit();
+
+    Duration getFrequencyPeriod();
+
+    @Override
+    default Map<Duration, Integer> getFrequencyLimits()
+    {
+        if (getFrequencyLimit() != null)
+        {
+            return Collections.singletonMap(getFrequencyPeriod(), getFrequencyLimit());
+        }
+        else
+        {
+            return Collections.emptyMap();
+        }
+    }
+
+    void updateWithDefaultFrequencyPeriod(Duration period);
+
+    static Rule newInstance(ConnectionLimitRule rule)
+    {
+        if (Boolean.TRUE.equals(rule.getBlocked()))
+        {
+            return new BlockingRule(rule);
+        }
+        else
+        {
+            return new NonBlockingRule(rule);
+        }
+    }
+
+    static Rule newInstance(String identity, RulePredicates predicates)
+    {
+        if (predicates.isUserBlocked())
+        {
+            return newBlockingRule(predicates.getPort(), identity);
+        }
+        else
+        {
+            return newNonBlockingRule(
+                    predicates.getPort(),
+                    identity,
+                    predicates.getConnectionCountLimit(),
+                    predicates.getConnectionFrequencyLimit(),
+                    predicates.getConnectionFrequencyPeriod());
+        }
+    }
+
+    static NonBlockingRule newNonBlockingRule(
+            String port, String identity, Integer count, Integer frequency, Duration frequencyPeriod)
+    {
+        return new NonBlockingRule(port, identity, count, frequency, frequencyPeriod);
+    }
+
+    static BlockingRule newBlockingRule(String port, String identity)
+    {
+        return new BlockingRule(port, identity);
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/RulePredicates.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/RulePredicates.java
new file mode 100644
index 0000000..d2037d7
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/RulePredicates.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.time.Duration;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class RulePredicates
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(RulePredicates.class);
+
+    public static final String ALL_PORTS = "ALL";
+    public static final String ALL_USERS = "ALL";
+
+    private static final String TIME_PERIOD_DELIMITER = "/";
+
+    private static final Duration HOUR = Duration.ofHours(1L);
+    private static final Duration MINUTE = Duration.ofMinutes(1L);
+    private static final Duration SECOND = Duration.ofSeconds(1L);
+
+    private Integer _connectionCountLimit;
+    private Integer _connectionFrequencyLimit;
+    private Duration _connectionFrequencyPeriod;
+    private boolean _blocked = false;
+
+    private final Set<Property> _properties = EnumSet.noneOf(Property.class);
+
+    private String _port = ALL_PORTS;
+
+    public static boolean isAllPort(String port)
+    {
+        return RulePredicates.ALL_PORTS.equalsIgnoreCase(port);
+    }
+
+    public static boolean isAllUser(String port)
+    {
+        return RulePredicates.ALL_USERS.equalsIgnoreCase(port);
+    }
+
+    public Property parse(String key, String value)
+    {
+        final Property property = Property.parse(key);
+
+        if (property != null)
+        {
+            addProperty(property, value);
+        }
+        return property;
+    }
+
+    void addProperty(Property property, String value)
+    {
+        checkPropertyAlreadyDefined(property);
+        switch (property)
+        {
+            case PORT:
+                _port = value.trim();
+                break;
+            case CONNECTION_LIMIT:
+                _connectionCountLimit = validateCounterLimit(Property.CONNECTION_LIMIT, Integer.parseInt(value));
+                break;
+            case CONNECTION_FREQUENCY_LIMIT:
+                addFrequencyLimit(value);
+                break;
+            default:
+        }
+        LOGGER.debug("Parsed {} with value {}", property, value);
+    }
+
+    private void addFrequencyLimit(String value)
+    {
+        if (value.contains(TIME_PERIOD_DELIMITER))
+        {
+            final String[] frequencyLimit = value.split(TIME_PERIOD_DELIMITER, 2);
+            _connectionFrequencyLimit = validateCounterLimit(
+                    Property.CONNECTION_FREQUENCY_LIMIT, Integer.parseInt(frequencyLimit[0]));
+            _connectionFrequencyPeriod = validateTimePeriod(parseTimePeriod(frequencyLimit[1]));
+        }
+        else
+        {
+            _connectionFrequencyLimit = validateCounterLimit(
+                    Property.CONNECTION_FREQUENCY_LIMIT, Integer.parseInt(value));
+            _connectionFrequencyPeriod = null;
+        }
+    }
+
+    private Duration validateTimePeriod(Duration period)
+    {
+        if (period != null && period.isNegative())
+        {
+            throw new IllegalArgumentException(
+                    String.format("Frequency period can not be negative %d s", period.getSeconds()));
+        }
+        return period;
+    }
+
+    private Duration parseTimePeriod(String period)
+    {
+        if ("Second".equalsIgnoreCase(period))
+        {
+            return SECOND;
+        }
+        if ("Minute".equalsIgnoreCase(period))
+        {
+            return MINUTE;
+        }
+        if ("Hour".equalsIgnoreCase(period))
+        {
+            return HOUR;
+        }
+        if (period.matches("\\d+.*"))
+        {
+            return Duration.parse("PT" + period);
+        }
+        if (period.matches("[hHmMsS]"))
+        {
+            return Duration.parse("PT1" + period);
+        }
+        return Duration.parse(period);
+    }
+
+    private Integer validateCounterLimit(Property property, int limit)
+    {
+        if (limit < 0)
+        {
+            LOGGER.debug("Negative value of {}, using {} instead", property, Integer.MAX_VALUE);
+            return Integer.MAX_VALUE;
+        }
+        return limit;
+    }
+
+    private void checkPropertyAlreadyDefined(Property property)
+    {
+        if (_properties.contains(property))
+        {
+            throw new IllegalStateException(String.format("Property '%s' has already been defined", property));
+        }
+        else
+        {
+            _properties.add(property);
+        }
+    }
+
+    public Integer getConnectionCountLimit()
+    {
+        return _connectionCountLimit;
+    }
+
+    public Integer getConnectionFrequencyLimit()
+    {
+        return _connectionFrequencyLimit;
+    }
+
+    public Duration getConnectionFrequencyPeriod()
+    {
+        return _connectionFrequencyPeriod;
+    }
+
+    public boolean isUserBlocked()
+    {
+        return _blocked;
+    }
+
+    public String getPort()
+    {
+        return _port;
+    }
+
+    public void setBlockedUser()
+    {
+        _blocked = true;
+    }
+
+    public boolean isEmpty()
+    {
+        return !_blocked && _connectionCountLimit == null && _connectionFrequencyLimit == null;
+    }
+
+    public enum Property
+    {
+        PORT,
+        CONNECTION_LIMIT,
+        CONNECTION_FREQUENCY_LIMIT;
+
+        private static final Map<String, Property> NAME_TO_PROPERTY = new HashMap<>();
+
+        static
+        {
+            for (final Property property : values())
+            {
+                NAME_TO_PROPERTY.put(property.name().toLowerCase(Locale.ENGLISH), property);
+                NAME_TO_PROPERTY.put(property.name()
+                        .replace("_", "").toLowerCase(Locale.ENGLISH), property);
+                NAME_TO_PROPERTY.put(property.name()
+                        .replace("_", "-").toLowerCase(Locale.ENGLISH), property);
+            }
+        }
+
+        public static Property parse(String text)
+        {
+            return NAME_TO_PROPERTY.get(text.toLowerCase(Locale.ENGLISH).trim());
+        }
+
+        @Override
+        public String toString()
+        {
+            return name().toLowerCase(Locale.ENGLISH);
+        }
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/RuleSet.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/RuleSet.java
new file mode 100644
index 0000000..40c3583
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/RuleSet.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.time.Duration;
+import java.util.Collection;
+
+import org.apache.qpid.server.security.limit.ConnectionLimiter;
+
+public interface RuleSet extends ConnectionLimiter
+{
+    static Builder newBuilder(String name, Duration defaultFrequencyPeriod)
+    {
+        return new RuleSetImpl.RuleSetBuilderImpl(name, defaultFrequencyPeriod);
+    }
+
+    interface Builder
+    {
+        Builder logAllMessages(boolean all);
+
+        Builder addRule(Rule rule);
+
+        Builder addRules(Collection<? extends Rule> rules);
+
+        RuleSet build();
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/RuleSetCreator.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/RuleSetCreator.java
new file mode 100644
index 0000000..6596aa8
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/RuleSetCreator.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Optional;
+
+import org.apache.qpid.server.security.limit.ConnectionLimitProvider;
+
+public final class RuleSetCreator extends ArrayList<Rule>
+{
+    private static final long serialVersionUID = -7;
+
+    private Long _defaultFrequencyPeriod = null;
+
+    private boolean _logAllMessages = false;
+
+    public RuleSetCreator()
+    {
+        super(new ArrayList<>());
+    }
+
+    public void setDefaultFrequencyPeriod(long defaultFrequencyPeriod)
+    {
+        _defaultFrequencyPeriod = defaultFrequencyPeriod;
+    }
+
+    public Long getDefaultFrequencyPeriod()
+    {
+        return _defaultFrequencyPeriod;
+    }
+
+    public boolean isDefaultFrequencyPeriodSet()
+    {
+        return _defaultFrequencyPeriod != null;
+    }
+
+    public void setLogAllMessages(boolean logAllMessages)
+    {
+        this._logAllMessages = logAllMessages;
+    }
+
+    public boolean isLogAllMessages()
+    {
+        return _logAllMessages;
+    }
+
+    public RuleSet getLimiter(String name)
+    {
+        final long period = Optional.ofNullable(_defaultFrequencyPeriod)
+                .orElse(ConnectionLimitProvider.DEFAULT_CONNECTION_FREQUENCY_PERIOD);
+        if (period > 0)
+        {
+            final Duration defaultFrequencyPeriod = Duration.ofMillis(period);
+            updateRulesWithDefaultFrequencyPeriod(defaultFrequencyPeriod);
+            return RuleSet.newBuilder(name, defaultFrequencyPeriod)
+                    .logAllMessages(_logAllMessages).addRules(this).build();
+        }
+        return RuleSet.newBuilder(name, null)
+                .logAllMessages(_logAllMessages).addRules(this).build();
+    }
+
+    public RuleSetCreator updateRulesWithDefaultFrequencyPeriod()
+    {
+        if (_defaultFrequencyPeriod != null && _defaultFrequencyPeriod > 0)
+        {
+            updateRulesWithDefaultFrequencyPeriod(Duration.ofMillis(_defaultFrequencyPeriod));
+        }
+        return this;
+    }
+
+    @Override
+    public boolean add(Rule rule)
+    {
+        return addImpl(rule);
+    }
+
+    @Override
+    public boolean addAll(Collection<? extends Rule> c)
+    {
+        boolean changed = false;
+        if (c != null)
+        {
+            for (final Rule rule : c)
+            {
+                changed = add(rule) || changed;
+            }
+        }
+        return changed;
+    }
+
+    private boolean addImpl(Rule rule)
+    {
+        if (rule != null && !rule.isEmpty())
+        {
+            return super.add(rule);
+        }
+        return false;
+    }
+
+    private void updateRulesWithDefaultFrequencyPeriod(final Duration defaultFrequencyPeriod)
+    {
+        forEach(r -> r.updateWithDefaultFrequencyPeriod(defaultFrequencyPeriod));
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/RuleSetImpl.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/RuleSetImpl.java
new file mode 100644
index 0000000..efd5d3f
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/RuleSetImpl.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.qpid.server.logging.EventLoggerProvider;
+import org.apache.qpid.server.security.limit.ConnectionLimitException;
+import org.apache.qpid.server.security.limit.ConnectionLimiter;
+import org.apache.qpid.server.security.limit.ConnectionSlot;
+import org.apache.qpid.server.transport.AMQPConnection;
+import org.apache.qpid.server.user.connection.limits.logging.ConnectionLimitEventLogger;
+import org.apache.qpid.server.user.connection.limits.logging.FullConnectionLimitEventLogger;
+import org.apache.qpid.server.user.connection.limits.outcome.RejectRegistration;
+
+final class RuleSetImpl implements RuleSet
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(RuleSet.class);
+
+    private final String _name;
+
+    private final Function<EventLoggerProvider, ConnectionLimitEventLogger> _loggerFactory;
+
+    private final Map<String, PortConnectionCounter> _portConnectionCounters;
+
+    private final Function<String, PortConnectionCounter> _defaultPortConnectionCounterProvider;
+
+    private final AtomicReference<ConnectionLimiter> _appendedLimiter;
+
+    RuleSetImpl(RuleSetBuilderImpl builder)
+    {
+        _name = builder.getName();
+        _loggerFactory = builder.getLoggerFactory();
+        _portConnectionCounters = new ConcurrentHashMap<>(builder.getPortConnectionCounters());
+        _defaultPortConnectionCounterProvider = builder.getDefaultPortConnectionCounterProvider();
+        _appendedLimiter = new AtomicReference<>(ConnectionLimiter.noLimits());
+    }
+
+    private RuleSetImpl(RuleSetImpl ruleSet, ConnectionLimiter limiter)
+    {
+        _name = ruleSet._name;
+        _loggerFactory = ruleSet._loggerFactory;
+        _portConnectionCounters = ruleSet._portConnectionCounters;
+        _defaultPortConnectionCounterProvider = ruleSet._defaultPortConnectionCounterProvider;
+        _appendedLimiter = new AtomicReference<>(limiter);
+    }
+
+    @Override
+    public ConnectionSlot register(AMQPConnection<?> connection)
+    {
+        try
+        {
+            return _portConnectionCounters.computeIfAbsent(connection.getPort().getName(), _defaultPortConnectionCounterProvider)
+                    .register(connection, _appendedLimiter.get())
+                    .logMessage(_loggerFactory.apply(connection));
+        }
+        catch (RejectRegistration result)
+        {
+            LOGGER.debug(String.format("Connection limiter %s, user limit exceeded", _name), result);
+            throw new ConnectionLimitException(result.logMessage(_loggerFactory.apply(connection)));
+        }
+    }
+
+    @Override
+    public ConnectionLimiter append(final ConnectionLimiter limiter)
+    {
+        return new RuleSetImpl(this, _appendedLimiter.get().append(limiter));
+    }
+
+    @Override
+    public String toString()
+    {
+        return _name;
+    }
+
+    static final class RuleSetBuilderImpl implements Builder
+    {
+        private final String _name;
+
+        private final List<Rule> _forAllPorts = new ArrayList<>();
+
+        private final Duration _defaultFrequencyPeriod;
+
+        private final Map<String, PortConnectionCounter.Builder> _builders;
+
+        private final PortConnectionCounter.Builder _defaultBuilder;
+
+        private Function<EventLoggerProvider, ConnectionLimitEventLogger> _loggerFactory;
+
+        RuleSetBuilderImpl(String name, Duration defaultFrequencyPeriod)
+        {
+            super();
+            _name = name;
+            _defaultFrequencyPeriod = defaultFrequencyPeriod;
+            _loggerFactory = loggerProvider -> new ConnectionLimitEventLogger(name, loggerProvider);
+
+            _builders = new HashMap<>();
+            _defaultBuilder = PortConnectionCounter.newBuilder(defaultFrequencyPeriod);
+        }
+
+        @Override
+        public Builder logAllMessages(boolean all)
+        {
+            if (all)
+            {
+                _loggerFactory = loggerProvider -> new FullConnectionLimitEventLogger(_name, loggerProvider);
+            }
+            else
+            {
+                _loggerFactory = loggerProvider -> new ConnectionLimitEventLogger(_name, loggerProvider);
+            }
+            return this;
+        }
+
+        @Override
+        public Builder addRule(Rule rule)
+        {
+            addRuleImpl(rule);
+            return this;
+        }
+
+        @Override
+        public Builder addRules(Collection<? extends Rule> rules)
+        {
+            if (rules != null)
+            {
+                rules.forEach(this::addRuleImpl);
+            }
+            return this;
+        }
+
+        private void addRuleImpl(Rule rule)
+        {
+            if (rule == null)
+            {
+                return;
+            }
+
+            final String port = rule.getPort();
+            if (RulePredicates.isAllPort(port))
+            {
+                _forAllPorts.add(rule);
+                _defaultBuilder.add(rule);
+                _builders.values().forEach(builder -> builder.add(rule));
+            }
+            else
+            {
+                _builders.computeIfAbsent(
+                        port,
+                        portName -> PortConnectionCounter.newBuilder(_defaultFrequencyPeriod).addAll(_forAllPorts)
+                ).add(rule);
+            }
+        }
+
+        @Override
+        public RuleSet build()
+        {
+            return new RuleSetImpl(this);
+        }
+
+        Function<EventLoggerProvider, ConnectionLimitEventLogger> getLoggerFactory()
+        {
+            return _loggerFactory;
+        }
+
+        Map<String, PortConnectionCounter> getPortConnectionCounters()
+        {
+            final Map<String, PortConnectionCounter> limiters = new HashMap<>();
+            _builders.forEach((port, builder) -> limiters.put(port, builder.build()));
+            return limiters;
+        }
+
+        Function<String, PortConnectionCounter> getDefaultPortConnectionCounterProvider()
+        {
+            return portName -> _defaultBuilder.build();
+        }
+
+        String getName()
+        {
+            return _name;
+        }
+    }
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/package-info.java
similarity index 82%
rename from broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
rename to broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/package-info.java
index 661b89b..30c36cd 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/config/package-info.java
@@ -16,12 +16,4 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-package org.apache.qpid.server.virtualhost;
-
-public interface ConnectionPrincipalStatistics
-{
-    int getConnectionCount();
-
-    int getConnectionFrequency();
-}
+package org.apache.qpid.server.user.connection.limits.config;
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/logging/ConnectionLimitEventLogger.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/logging/ConnectionLimitEventLogger.java
new file mode 100644
index 0000000..0f7522b
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/logging/ConnectionLimitEventLogger.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.logging;
+
+import java.util.Objects;
+
+import org.apache.qpid.server.logging.EventLoggerProvider;
+import org.apache.qpid.server.logging.messages.ResourceLimitMessages;
+
+public class ConnectionLimitEventLogger
+{
+    static final String OPENING = "Opening";
+    static final String CONNECTION = "connection";
+    static final String MESSAGE = "Limiter '%s': %s";
+
+    final EventLoggerProvider _logger;
+
+    final String _sourceName;
+
+    public ConnectionLimitEventLogger(String name, EventLoggerProvider logger)
+    {
+        super();
+        _sourceName = Objects.requireNonNull(name);
+        _logger = Objects.requireNonNull(logger);
+    }
+
+    public void logRejectConnection(String userId, String reason)
+    {
+        _logger.getEventLogger().message(ResourceLimitMessages.REJECTED(
+                OPENING, CONNECTION, userId, String.format(MESSAGE, _sourceName, reason)));
+    }
+
+    public void logAcceptConnection(String userId, String message)
+    {
+        // Do nothing
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/logging/FullConnectionLimitEventLogger.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/logging/FullConnectionLimitEventLogger.java
new file mode 100644
index 0000000..28b66ed
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/logging/FullConnectionLimitEventLogger.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.logging;
+
+import org.apache.qpid.server.logging.EventLoggerProvider;
+import org.apache.qpid.server.logging.messages.ResourceLimitMessages;
+
+public final class FullConnectionLimitEventLogger extends ConnectionLimitEventLogger
+{
+    public FullConnectionLimitEventLogger(String name, EventLoggerProvider logger)
+    {
+        super(name, logger);
+    }
+
+    @Override
+    public void logAcceptConnection(String userId, String message)
+    {
+        _logger.getEventLogger().message(ResourceLimitMessages.ACCEPTED(
+                OPENING, CONNECTION, userId, String.format(MESSAGE, _sourceName, message)));
+    }
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/logging/package-info.java
similarity index 82%
copy from broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
copy to broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/logging/package-info.java
index 661b89b..9051bf8 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/logging/package-info.java
@@ -16,12 +16,4 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-package org.apache.qpid.server.virtualhost;
-
-public interface ConnectionPrincipalStatistics
-{
-    int getConnectionCount();
-
-    int getConnectionFrequency();
-}
+package org.apache.qpid.server.user.connection.limits.logging;
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/outcome/AcceptRegistration.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/outcome/AcceptRegistration.java
new file mode 100644
index 0000000..4342c17
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/outcome/AcceptRegistration.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.outcome;
+
+import java.util.Objects;
+
+import org.apache.qpid.server.security.limit.ConnectionSlot;
+import org.apache.qpid.server.user.connection.limits.logging.ConnectionLimitEventLogger;
+
+public final class AcceptRegistration implements ConnectionSlot
+{
+    private static final String USER_WITH_COUNT_ON_PORT = "User %s with connection count %d on %s port";
+
+    private final String _message;
+
+    private final String _userId;
+
+    private final ConnectionSlot _slot;
+
+    public static AcceptRegistration newInstance(ConnectionSlot slot, String userId, long currentCount, String port)
+    {
+        return new AcceptRegistration(slot, userId, String.format(USER_WITH_COUNT_ON_PORT, userId, currentCount, port));
+    }
+
+    private AcceptRegistration(ConnectionSlot slot, String userId, String message)
+    {
+        super();
+        this._slot = Objects.requireNonNull(slot);
+        this._userId = Objects.requireNonNull(userId);
+        this._message = Objects.requireNonNull(message);
+    }
+
+    public ConnectionSlot logMessage(ConnectionLimitEventLogger logger)
+    {
+        logger.logAcceptConnection(_userId, getMessage());
+        return _slot;
+    }
+
+
+    @Override
+    public void free()
+    {
+        _slot.free();
+    }
+
+    protected String getMessage()
+    {
+        return _message;
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/outcome/RejectRegistration.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/outcome/RejectRegistration.java
new file mode 100644
index 0000000..641733e
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/outcome/RejectRegistration.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.outcome;
+
+import java.time.Duration;
+
+import org.apache.qpid.server.security.limit.ConnectionLimitException;
+import org.apache.qpid.server.user.connection.limits.logging.ConnectionLimitEventLogger;
+
+public final class RejectRegistration extends ConnectionLimitException
+{
+    private static final String USER_COUNT_BREAKS_LIMIT_ON_PORT =
+            "User %s breaks connection count limit %d on port %s";
+    private static final String USER_FREQUENCY_BREAKS_LIMIT_ON_PORT =
+            "User %s breaks connection frequency limit %d per %d s on port %s";
+    private static final String USER_BLOCKED_ON_PORT = "User %s is blocked on port %s";
+
+    private final String _userId;
+
+    public static RejectRegistration breakingConnectionCount(String userId, int limit, String port)
+    {
+        return new RejectRegistration(userId,
+                String.format(USER_COUNT_BREAKS_LIMIT_ON_PORT, userId, limit, port));
+    }
+
+    public static RejectRegistration breakingConnectionFrequency(
+            String userId, int limit, Duration frequencyPeriod, String port)
+    {
+        return new RejectRegistration(userId, String.format(
+                USER_FREQUENCY_BREAKS_LIMIT_ON_PORT, userId, limit, frequencyPeriod.getSeconds(), port));
+    }
+
+    public static RejectRegistration blockedUser(String userId, String port)
+    {
+        return new RejectRegistration(userId,
+                String.format(USER_BLOCKED_ON_PORT, userId, port));
+    }
+
+    public String logMessage(ConnectionLimitEventLogger logger)
+    {
+        final String message = getMessage();
+        logger.logRejectConnection(_userId, message);
+        return message;
+    }
+
+    private RejectRegistration(String userId, String message)
+    {
+        super(message);
+        this._userId = userId;
+    }
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/outcome/package-info.java
similarity index 82%
copy from broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
copy to broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/outcome/package-info.java
index 661b89b..83cdd57 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/outcome/package-info.java
@@ -16,12 +16,4 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-package org.apache.qpid.server.virtualhost;
-
-public interface ConnectionPrincipalStatistics
-{
-    int getConnectionCount();
-
-    int getConnectionFrequency();
-}
+package org.apache.qpid.server.user.connection.limits.outcome;
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/AbstractConnectionLimitProvider.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/AbstractConnectionLimitProvider.java
new file mode 100644
index 0000000..22e0367
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/AbstractConnectionLimitProvider.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.plugins;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.qpid.server.configuration.IllegalConfigurationException;
+import org.apache.qpid.server.model.AbstractConfiguredObject;
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.State;
+import org.apache.qpid.server.model.StateTransition;
+import org.apache.qpid.server.model.SystemConfig;
+import org.apache.qpid.server.security.limit.ConnectionLimitProvider;
+import org.apache.qpid.server.security.limit.ConnectionLimiter;
+import org.apache.qpid.server.user.connection.limits.config.RuleSetCreator;
+import org.apache.qpid.server.util.urlstreamhandler.data.Handler;
+
+abstract class AbstractConnectionLimitProvider<X extends AbstractConnectionLimitProvider<X>>
+        extends AbstractConfiguredObject<X> implements ConnectionLimitProvider<X>
+{
+    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractConnectionLimitProvider.class);
+
+    private static final String FAILED_CREATE_NEW_PROVIDER = "Failed to create a new connection limit provider";
+
+    private final AtomicReference<RuleSetCreator> _creator = new AtomicReference<>(null);
+
+    static
+    {
+        Handler.register();
+    }
+
+    abstract RuleSetCreator newRuleSetCreator();
+
+    public AbstractConnectionLimitProvider(ConfiguredObject<?> parent, Map<String, Object> attributes)
+    {
+        super(parent, attributes);
+    }
+
+    @Override
+    public ConnectionLimiter getConnectionLimiter()
+    {
+        return Optional.ofNullable(_creator.get())
+                .<ConnectionLimiter>map(provider -> provider.getLimiter(getName()))
+                .orElseGet(ConnectionLimiter::noLimits);
+    }
+
+    @Override
+    public void onValidate()
+    {
+        super.onValidate();
+        if (!isDurable())
+        {
+            throw new IllegalArgumentException(getClass().getSimpleName() + " must be durable");
+        }
+    }
+
+    @Override
+    protected void validateOnCreate()
+    {
+        try
+        {
+            if (_creator.get() == null)
+            {
+                _creator.compareAndSet(null, newRuleSetCreator());
+            }
+        }
+        catch (RuntimeException e)
+        {
+            throw new IllegalConfigurationException(FAILED_CREATE_NEW_PROVIDER, e);
+        }
+    }
+
+    @StateTransition(currentState = {State.UNINITIALIZED, State.QUIESCED, State.ERRORED}, desiredState = State.ACTIVE)
+    @SuppressWarnings("unused")
+    ListenableFuture<Void> activate()
+    {
+        final boolean isManagementMode = getModel().getAncestor(SystemConfig.class, this).isManagementMode();
+        final RuleSetCreator creator;
+        if (State.ERRORED == getState())
+        {
+            creator = null;
+            _creator.set(null);
+        }
+        else
+        {
+            creator = _creator.get();
+        }
+        try
+        {
+            if (creator == null)
+            {
+                _creator.compareAndSet(null, newRuleSetCreator());
+            }
+            setState(isManagementMode ? State.QUIESCED : State.ACTIVE);
+        }
+        catch (RuntimeException e)
+        {
+            LOGGER.debug(String.format(
+                    "Connection limit provider '%s' can not be activated because of the error: ", getName()), e);
+            setState(State.ERRORED);
+            if (isManagementMode)
+            {
+                LOGGER.warn(String.format("Failed to activate connection limit provider: %s", getName()));
+            }
+            else
+            {
+                throw e;
+            }
+        }
+        return Futures.immediateFuture(null);
+    }
+
+    @StateTransition(currentState = {
+            State.UNINITIALIZED, State.QUIESCED, State.ACTIVE, State.STOPPED, State.DELETED, State.UNAVAILABLE},
+            desiredState = State.ERRORED)
+    @SuppressWarnings("unused")
+    ListenableFuture<Void> error()
+    {
+        _creator.set(null);
+        setState(State.ERRORED);
+        return Futures.immediateFuture(null);
+    }
+
+    @StateTransition(currentState = State.UNINITIALIZED, desiredState = State.QUIESCED)
+    @SuppressWarnings("unused")
+    private ListenableFuture<Void> startQuiesced()
+    {
+        setState(State.QUIESCED);
+        return Futures.immediateFuture(null);
+    }
+
+    protected void forceNewRuleSetCreator()
+    {
+        try
+        {
+            _creator.set(newRuleSetCreator());
+        }
+        catch (RuntimeException e)
+        {
+            _creator.set(null);
+            throw new IllegalConfigurationException(FAILED_CREATE_NEW_PROVIDER, e);
+        }
+    }
+
+    protected RuleSetCreator creator()
+    {
+        return _creator.get();
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/AbstractFileBasedConnectionLimitProvider.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/AbstractFileBasedConnectionLimitProvider.java
new file mode 100644
index 0000000..07d2c9b
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/AbstractFileBasedConnectionLimitProvider.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.plugins;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.apache.qpid.server.configuration.IllegalConfigurationException;
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.ManagedAttributeField;
+import org.apache.qpid.server.security.access.Operation;
+import org.apache.qpid.server.security.limit.ConnectionLimitProvider;
+import org.apache.qpid.server.user.connection.limits.config.FileParser;
+import org.apache.qpid.server.user.connection.limits.config.Rule;
+import org.apache.qpid.server.user.connection.limits.config.RuleSetCreator;
+
+abstract class AbstractFileBasedConnectionLimitProvider<C extends AbstractFileBasedConnectionLimitProvider<C>>
+        extends AbstractConnectionLimitProvider<C> implements FileBasedConnectionLimitProvider<C>
+{
+    static final String FILE_PROVIDER_TYPE = "ConnectionLimitFile";
+
+    @ManagedAttributeField(afterSet = "reloadSourceFile")
+    private String _path;
+
+    public AbstractFileBasedConnectionLimitProvider(ConfiguredObject<?> parent, Map<String, Object> attributes)
+    {
+        super(parent, attributes);
+    }
+
+    @Override
+    public String getPath()
+    {
+        return _path;
+    }
+
+    @Override
+    public void reload()
+    {
+        authorise(Operation.UPDATE);
+        reloadSourceFile();
+    }
+
+    @Override
+    public Long getDefaultFrequencyPeriod()
+    {
+        return Optional.ofNullable(creator())
+                .map(RuleSetCreator::getDefaultFrequencyPeriod)
+                .orElseGet(() -> getContextValue(Long.class, ConnectionLimitProvider.CONNECTION_FREQUENCY_PERIOD));
+    }
+
+    @Override
+    public List<ConnectionLimitRule> getRules()
+    {
+        return Optional.<List<Rule>>ofNullable(creator()).orElseGet(Collections::emptyList)
+                .stream().map(ConnectionLimitRuleImpl::new).collect(Collectors.toList());
+    }
+
+    @Override
+    public void resetCounters()
+    {
+        changeAttributes(Collections.emptyMap());
+    }
+
+    private void reloadSourceFile()
+    {
+        try
+        {
+            forceNewRuleSetCreator();
+            // force the change listener to fire, causing the parent broker to update its cache
+            changeAttributes(Collections.emptyMap());
+        }
+        catch (RuntimeException e)
+        {
+            throw new IllegalConfigurationException("Failed to reload the connection limit file", e);
+        }
+    }
+
+    @Override
+    protected RuleSetCreator newRuleSetCreator()
+    {
+        final RuleSetCreator creator = FileParser.parse(getPath());
+        if (!creator.isDefaultFrequencyPeriodSet())
+        {
+            creator.setDefaultFrequencyPeriod(
+                    getContextValue(Long.class, ConnectionLimitProvider.CONNECTION_FREQUENCY_PERIOD));
+        }
+        return creator;
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/AbstractRuleBasedConnectionLimitProvider.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/AbstractRuleBasedConnectionLimitProvider.java
new file mode 100644
index 0000000..4ff622f
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/AbstractRuleBasedConnectionLimitProvider.java
@@ -0,0 +1,232 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.plugins;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.Content;
+import org.apache.qpid.server.model.CustomRestHeaders;
+import org.apache.qpid.server.model.ManagedAttributeField;
+import org.apache.qpid.server.model.RestContentHeader;
+import org.apache.qpid.server.security.access.Operation;
+import org.apache.qpid.server.security.limit.ConnectionLimitProvider;
+import org.apache.qpid.server.user.connection.limits.config.FileParser;
+import org.apache.qpid.server.user.connection.limits.config.Rule;
+import org.apache.qpid.server.user.connection.limits.config.RulePredicates.Property;
+import org.apache.qpid.server.user.connection.limits.config.RuleSetCreator;
+
+abstract class AbstractRuleBasedConnectionLimitProvider<C extends AbstractRuleBasedConnectionLimitProvider<C>>
+        extends AbstractConnectionLimitProvider<C> implements RuleBasedConnectionLimitProvider<C>
+{
+    private static final String RULES = "rules";
+
+    static final String RULE_BASED_TYPE = "RuleBased";
+
+    @ManagedAttributeField
+    private Long _defaultFrequencyPeriod;
+
+    @ManagedAttributeField
+    private List<ConnectionLimitRule> _rules = new ArrayList<>();
+
+    public AbstractRuleBasedConnectionLimitProvider(ConfiguredObject<?> parent, Map<String, Object> attributes)
+    {
+        super(parent, attributes);
+    }
+
+    @Override
+    public List<ConnectionLimitRule> getRules()
+    {
+        return Collections.unmodifiableList(_rules);
+    }
+
+    @Override
+    public Long getDefaultFrequencyPeriod()
+    {
+        return Optional.ofNullable(_defaultFrequencyPeriod).orElseGet(
+                () -> getContextValue(Long.class, ConnectionLimitProvider.CONNECTION_FREQUENCY_PERIOD));
+    }
+
+    @Override
+    public Content extractRules()
+    {
+        return new StringContent(getName(), getDefaultFrequencyPeriod(), getRules());
+    }
+
+    @Override
+    public void loadFromFile(String path)
+    {
+        authorise(Operation.UPDATE);
+
+        final List<ConnectionLimitRule> connectionLimitRules = new ArrayList<>(getRules());
+        for (final Rule rule : FileParser.parse(path).updateRulesWithDefaultFrequencyPeriod())
+        {
+            connectionLimitRules.add(new ConnectionLimitRuleImpl(rule));
+        }
+        changeAttributes(Collections.singletonMap(RULES, connectionLimitRules));
+    }
+
+    @Override
+    public void clearRules()
+    {
+        authorise(Operation.UPDATE);
+        changeAttributes(Collections.singletonMap(RULES, new ArrayList<ConnectionLimitRule>()));
+    }
+
+    @Override
+    public void resetCounters()
+    {
+        changeAttributes(Collections.emptyMap());
+    }
+
+    @Override
+    protected void postSetAttributes(final Set<String> actualUpdatedAttributes)
+    {
+        super.postSetAttributes(actualUpdatedAttributes);
+        forceNewRuleSetCreator();
+    }
+
+    @Override
+    protected RuleSetCreator newRuleSetCreator()
+    {
+        final RuleSetCreator creator = new RuleSetCreator();
+        creator.setDefaultFrequencyPeriod(getDefaultFrequencyPeriod());
+        for (final ConnectionLimitRule rule : getRules())
+        {
+            creator.add(Rule.newInstance(rule));
+        }
+        return creator;
+    }
+
+    private static final class StringContent implements Content, CustomRestHeaders
+    {
+        private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd-HHmmss");
+        private final String _name;
+        private final long _defaultFrequencyPeriod;
+        private final List<ConnectionLimitRule> _rules;
+
+        StringContent(String name, long defaultFrequencyPeriod, List<? extends ConnectionLimitRule> rules)
+        {
+            _name = Objects.requireNonNull(name);
+            _defaultFrequencyPeriod = defaultFrequencyPeriod;
+            _rules = new ArrayList<>(rules);
+        }
+
+        @Override
+        public void write(final OutputStream outputStream) throws IOException
+        {
+            final byte[] lineSeparator = System.lineSeparator().getBytes(StandardCharsets.UTF_8);
+
+            outputStream.write(
+                    String.format("CONFIG %s=%d", FileParser.DEFAULT_FREQUENCY_PERIOD, _defaultFrequencyPeriod)
+                            .getBytes(StandardCharsets.UTF_8));
+            outputStream.write(lineSeparator);
+
+            for (final ConnectionLimitRule rule : _rules)
+            {
+                outputStream.write(convertToString(rule).getBytes(StandardCharsets.UTF_8));
+                outputStream.write(lineSeparator);
+            }
+        }
+
+        @RestContentHeader("Content-Type")
+        public String getContentType()
+        {
+            return "text/plain";
+        }
+
+        @RestContentHeader("Content-Disposition")
+        public String getContentDisposition()
+        {
+            return String.format("attachment; filename=\"%s-%s.clt\"", _name, FORMATTER.format(LocalDateTime.now()));
+        }
+
+        @Override
+        public void release()
+        {
+            // Nothing to do
+        }
+
+        private String convertToString(ConnectionLimitRule rule)
+        {
+            final String prefix = FileParser.CONNECTION_LIMIT.toUpperCase(Locale.ENGLISH);
+            final StringBuilder builder = new StringBuilder();
+            builder.append(String.format("%s %s", prefix, rule.getIdentity()));
+            if (Boolean.TRUE.equals(rule.getBlocked()))
+            {
+                builder.append(String.format(" %s", FileParser.BLOCK));
+            }
+            else
+            {
+                appendCountLimit(builder, rule);
+                appendFrequencyLimit(builder, rule);
+            }
+            appendPort(builder, rule);
+            return builder.toString();
+        }
+
+        private void appendPort(StringBuilder builder, ConnectionLimitRule rule)
+        {
+            if (rule.getPort() != null)
+            {
+                builder.append(String.format(" %s=%s", Property.PORT, rule.getPort()));
+            }
+        }
+
+        private void appendFrequencyLimit(StringBuilder builder, ConnectionLimitRule rule)
+        {
+            if (rule.getFrequencyLimit() != null)
+            {
+                if (rule.getFrequencyPeriod() == null)
+                {
+                    builder.append(
+                            String.format(" %s=%d", Property.CONNECTION_FREQUENCY_LIMIT, rule.getFrequencyLimit()));
+                }
+                else
+                {
+                    builder.append(String.format(" %s=%d/%s",
+                            Property.CONNECTION_FREQUENCY_LIMIT,
+                            rule.getFrequencyLimit(),
+                            Duration.ofMillis(rule.getFrequencyPeriod())));
+                }
+            }
+        }
+
+        private void appendCountLimit(StringBuilder builder, ConnectionLimitRule rule)
+        {
+            if (rule.getCountLimit() != null)
+            {
+                builder.append(String.format(" %s=%d", Property.CONNECTION_LIMIT, rule.getCountLimit()));
+            }
+        }
+    }
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatisticsRegistry.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/ConnectionLimitRule.java
similarity index 64%
rename from broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatisticsRegistry.java
rename to broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/ConnectionLimitRule.java
index 2f0afba..1a56b69 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatisticsRegistry.java
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/ConnectionLimitRule.java
@@ -16,18 +16,23 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.qpid.server.user.connection.limits.plugins;
 
-package org.apache.qpid.server.virtualhost;
+import org.apache.qpid.server.model.ManagedAttributeValue;
+import org.apache.qpid.server.model.ManagedAttributeValueType;
 
-import org.apache.qpid.server.transport.AMQPConnection;
-
-public interface ConnectionPrincipalStatisticsRegistry
+@ManagedAttributeValueType
+public interface ConnectionLimitRule extends ManagedAttributeValue
 {
-    ConnectionPrincipalStatistics connectionOpened(AMQPConnection<?> connection);
+    String getPort();
 
-    ConnectionPrincipalStatistics connectionClosed(AMQPConnection<?> connection);
+    String getIdentity();
 
-    void reevaluateConnectionStatistics();
+    Boolean getBlocked();
 
-    void reset();
+    Integer getCountLimit();
+
+    Integer getFrequencyLimit();
+
+    Long getFrequencyPeriod();
 }
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/ConnectionLimitRuleImpl.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/ConnectionLimitRuleImpl.java
new file mode 100644
index 0000000..3a3ec59
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/ConnectionLimitRuleImpl.java
@@ -0,0 +1,65 @@
+package org.apache.qpid.server.user.connection.limits.plugins;
+
+import java.time.Duration;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.apache.qpid.server.user.connection.limits.config.Rule;
+
+class ConnectionLimitRuleImpl implements ConnectionLimitRule
+{
+    private final Rule _rule;
+
+    ConnectionLimitRuleImpl(Rule rule)
+    {
+        _rule = Objects.requireNonNull(rule);
+    }
+
+    @Override
+    public String getPort()
+    {
+        return _rule.getPort();
+    }
+
+    @Override
+    public String getIdentity()
+    {
+        return _rule.getIdentity();
+    }
+
+    @Override
+    public Boolean getBlocked()
+    {
+        return _rule.isUserBlocked();
+    }
+
+    @Override
+    public Integer getCountLimit()
+    {
+        if (!_rule.isUserBlocked())
+        {
+            return _rule.getCountLimit();
+        }
+        return null;
+    }
+
+    @Override
+    public Integer getFrequencyLimit()
+    {
+        if (!_rule.isUserBlocked())
+        {
+            return _rule.getFrequencyLimit();
+        }
+        return null;
+    }
+
+    @Override
+    public Long getFrequencyPeriod()
+    {
+        if (!_rule.isUserBlocked() && _rule.getFrequencyLimit() != null)
+        {
+            return Optional.ofNullable(_rule.getFrequencyPeriod()).map(Duration::toMillis).orElse(null);
+        }
+        return null;
+    }
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedBrokerConnectionLimitProvider.java
similarity index 62%
copy from broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
copy to broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedBrokerConnectionLimitProvider.java
index 661b89b..81a07a9 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedBrokerConnectionLimitProvider.java
@@ -16,12 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.qpid.server.user.connection.limits.plugins;
 
-package org.apache.qpid.server.virtualhost;
+import org.apache.qpid.server.model.BrokerConnectionLimitProvider;
+import org.apache.qpid.server.model.ManagedObject;
 
-public interface ConnectionPrincipalStatistics
+@ManagedObject(category = false,
+        type = AbstractFileBasedConnectionLimitProvider.FILE_PROVIDER_TYPE)
+public interface FileBasedBrokerConnectionLimitProvider<C extends FileBasedBrokerConnectionLimitProvider<C>>
+        extends FileBasedConnectionLimitProvider<C>, BrokerConnectionLimitProvider<C>
 {
-    int getConnectionCount();
-
-    int getConnectionFrequency();
 }
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedBrokerConnectionLimitProviderImpl.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedBrokerConnectionLimitProviderImpl.java
new file mode 100644
index 0000000..e05110b
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedBrokerConnectionLimitProviderImpl.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.plugins;
+
+import java.util.Map;
+
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.ManagedObjectFactoryConstructor;
+
+public class FileBasedBrokerConnectionLimitProviderImpl
+        extends AbstractFileBasedConnectionLimitProvider<FileBasedBrokerConnectionLimitProviderImpl>
+        implements FileBasedBrokerConnectionLimitProvider<FileBasedBrokerConnectionLimitProviderImpl>
+{
+    @ManagedObjectFactoryConstructor
+    public FileBasedBrokerConnectionLimitProviderImpl(Map<String, Object> attributes, ConfiguredObject<?> parent)
+    {
+        super(parent, attributes);
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedConnectionLimitProvider.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedConnectionLimitProvider.java
new file mode 100644
index 0000000..3745aaa
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedConnectionLimitProvider.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.plugins;
+
+import java.util.List;
+
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.DerivedAttribute;
+import org.apache.qpid.server.model.ManagedAttribute;
+import org.apache.qpid.server.model.ManagedObject;
+import org.apache.qpid.server.model.ManagedOperation;
+
+@ManagedObject(category = false)
+public interface FileBasedConnectionLimitProvider<X extends ConfiguredObject<X>> extends ConfiguredObject<X>
+{
+    @ManagedAttribute(mandatory = true, description = "File location", oversize = true,
+            oversizedAltText = ConfiguredObject.OVER_SIZED_ATTRIBUTE_ALTERNATIVE_TEXT)
+    String getPath();
+
+    @ManagedOperation(description = "Causes the connection limit rules to be reloaded. Changes are applied immediately.",
+            changesConfiguredObjectState = true)
+    void reload();
+
+    @ManagedOperation(nonModifying = true,
+            description = "Reset the counters of connections",
+            changesConfiguredObjectState = false)
+    void resetCounters();
+
+    @DerivedAttribute
+    Long getDefaultFrequencyPeriod();
+
+    @DerivedAttribute
+    List<ConnectionLimitRule> getRules();
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedVirtualHostConnectionLimitProvider.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedVirtualHostConnectionLimitProvider.java
new file mode 100644
index 0000000..576b6a3
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedVirtualHostConnectionLimitProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.plugins;
+
+import org.apache.qpid.server.model.ManagedObject;
+import org.apache.qpid.server.model.VirtualHostConnectionLimitProvider;
+
+@ManagedObject(category = false,
+        type = AbstractFileBasedConnectionLimitProvider.FILE_PROVIDER_TYPE,
+        amqpName = "org.apache.qpid.FileBasedVirtualHostConnectionLimitProvider")
+public interface FileBasedVirtualHostConnectionLimitProvider
+        <C extends FileBasedVirtualHostConnectionLimitProvider<C>>
+        extends FileBasedConnectionLimitProvider<C>, VirtualHostConnectionLimitProvider<C>
+{
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedVirtualHostConnectionLimitProviderImpl.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedVirtualHostConnectionLimitProviderImpl.java
new file mode 100644
index 0000000..12569a2
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/FileBasedVirtualHostConnectionLimitProviderImpl.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.plugins;
+
+import java.util.Map;
+
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.ManagedObjectFactoryConstructor;
+
+public class FileBasedVirtualHostConnectionLimitProviderImpl
+        extends AbstractFileBasedConnectionLimitProvider<FileBasedVirtualHostConnectionLimitProviderImpl>
+        implements FileBasedVirtualHostConnectionLimitProvider<FileBasedVirtualHostConnectionLimitProviderImpl>
+{
+    @ManagedObjectFactoryConstructor
+    public FileBasedVirtualHostConnectionLimitProviderImpl(Map<String, Object> attributes, ConfiguredObject<?> parent)
+    {
+        super(parent, attributes);
+    }
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedBrokerConnectionLimitProvider.java
similarity index 62%
copy from broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
copy to broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedBrokerConnectionLimitProvider.java
index 661b89b..428ceeb 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedBrokerConnectionLimitProvider.java
@@ -16,12 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.qpid.server.user.connection.limits.plugins;
 
-package org.apache.qpid.server.virtualhost;
+import org.apache.qpid.server.model.BrokerConnectionLimitProvider;
+import org.apache.qpid.server.model.ManagedObject;
 
-public interface ConnectionPrincipalStatistics
+@ManagedObject(category = false, type = AbstractRuleBasedConnectionLimitProvider.RULE_BASED_TYPE)
+public interface RuleBasedBrokerConnectionLimitProvider<C extends RuleBasedBrokerConnectionLimitProvider<C>>
+        extends RuleBasedConnectionLimitProvider<C>, BrokerConnectionLimitProvider<C>
 {
-    int getConnectionCount();
-
-    int getConnectionFrequency();
 }
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedBrokerConnectionLimitProviderImpl.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedBrokerConnectionLimitProviderImpl.java
new file mode 100644
index 0000000..67c0172
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedBrokerConnectionLimitProviderImpl.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.plugins;
+
+import java.util.Map;
+
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.ManagedObjectFactoryConstructor;
+
+public class RuleBasedBrokerConnectionLimitProviderImpl
+        extends AbstractRuleBasedConnectionLimitProvider<RuleBasedBrokerConnectionLimitProviderImpl>
+        implements RuleBasedBrokerConnectionLimitProvider<RuleBasedBrokerConnectionLimitProviderImpl>
+{
+    @ManagedObjectFactoryConstructor
+    public RuleBasedBrokerConnectionLimitProviderImpl(Map<String, Object> attributes, ConfiguredObject<?> parent)
+    {
+        super(parent, attributes);
+    }
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedConnectionLimitProvider.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedConnectionLimitProvider.java
new file mode 100644
index 0000000..135689d
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedConnectionLimitProvider.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.plugins;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.Content;
+import org.apache.qpid.server.model.ManagedAttribute;
+import org.apache.qpid.server.model.ManagedObject;
+import org.apache.qpid.server.model.ManagedOperation;
+import org.apache.qpid.server.model.Param;
+
+@ManagedObject(category = false)
+public interface RuleBasedConnectionLimitProvider<X extends ConfiguredObject<X>> extends ConfiguredObject<X>
+{
+    @ManagedAttribute(mandatory = true, defaultValue = "[ ]", description = "the list of the connection limits")
+    default List<ConnectionLimitRule> getRules()
+    {
+        return Collections.emptyList();
+    }
+
+    @ManagedAttribute
+    Long getDefaultFrequencyPeriod();
+
+    @ManagedOperation(nonModifying = true,
+            description = "Extract the connection limit rules",
+            changesConfiguredObjectState = false)
+    Content extractRules();
+
+    @ManagedOperation(description = "Load connection limit rules from a file", changesConfiguredObjectState = true)
+    void loadFromFile(@Param(name = "path", mandatory = true) String path);
+
+    @ManagedOperation(description = "Clear all connection limits", changesConfiguredObjectState = true)
+    void clearRules();
+
+    @ManagedOperation(nonModifying = true,
+            description = "Reset the counters of connections",
+            changesConfiguredObjectState = false)
+    void resetCounters();
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedVirtualHostConnectionLimitProvider.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedVirtualHostConnectionLimitProvider.java
new file mode 100644
index 0000000..7fbdeba
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedVirtualHostConnectionLimitProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.plugins;
+
+import org.apache.qpid.server.model.ManagedObject;
+import org.apache.qpid.server.model.VirtualHostConnectionLimitProvider;
+
+@ManagedObject(category = false,
+        type = AbstractRuleBasedConnectionLimitProvider.RULE_BASED_TYPE,
+        amqpName = "org.apache.qpid.RuleBasedVirtualHostConnectionLimitProvider")
+public interface RuleBasedVirtualHostConnectionLimitProvider
+        <C extends RuleBasedVirtualHostConnectionLimitProvider<C>>
+        extends RuleBasedConnectionLimitProvider<C>, VirtualHostConnectionLimitProvider<C>
+{
+}
diff --git a/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedVirtualHostConnectionLimitProviderImpl.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedVirtualHostConnectionLimitProviderImpl.java
new file mode 100644
index 0000000..2bd3258
--- /dev/null
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/RuleBasedVirtualHostConnectionLimitProviderImpl.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.plugins;
+
+import java.util.Map;
+
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.ManagedObjectFactoryConstructor;
+
+public class RuleBasedVirtualHostConnectionLimitProviderImpl
+        extends AbstractRuleBasedConnectionLimitProvider<RuleBasedVirtualHostConnectionLimitProviderImpl>
+        implements RuleBasedVirtualHostConnectionLimitProvider<RuleBasedVirtualHostConnectionLimitProviderImpl>
+{
+    @ManagedObjectFactoryConstructor
+    public RuleBasedVirtualHostConnectionLimitProviderImpl(
+            Map<String, Object> attributes, ConfiguredObject<?> parent)
+    {
+        super(parent, attributes);
+    }
+}
diff --git a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/package-info.java
similarity index 82%
copy from broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
copy to broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/package-info.java
index 661b89b..fc82f95 100644
--- a/broker-core/src/main/java/org/apache/qpid/server/virtualhost/ConnectionPrincipalStatistics.java
+++ b/broker-plugins/connection-limits/src/main/java/org/apache/qpid/server/user/connection/limits/plugins/package-info.java
@@ -16,12 +16,4 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-package org.apache.qpid.server.virtualhost;
-
-public interface ConnectionPrincipalStatistics
-{
-    int getConnectionCount();
-
-    int getConnectionFrequency();
-}
+package org.apache.qpid.server.user.connection.limits.plugins;
diff --git a/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/ConnectionCountLimitTest.java b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/ConnectionCountLimitTest.java
new file mode 100644
index 0000000..4b4149a
--- /dev/null
+++ b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/ConnectionCountLimitTest.java
@@ -0,0 +1,304 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import org.junit.Test;
+
+import org.apache.qpid.test.utils.UnitTestBase;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class ConnectionCountLimitTest extends UnitTestBase
+{
+    @Test
+    public void testMergeWith_NonBlocking()
+    {
+        final ConnectionCountLimit first = () -> null;
+        final ConnectionCountLimit second = () -> 20;
+
+        ConnectionCountLimit limits = first.mergeWith(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(20), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = second.mergeWith(first);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(20), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+    }
+
+    @Test
+    public void testMergeWith_Smaller()
+    {
+        final ConnectionCountLimit first = () -> 10;
+        final ConnectionCountLimit second = () -> 20;
+
+        ConnectionCountLimit limits = first.mergeWith(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(10), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = second.mergeWith(first);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(10), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+    }
+
+    @Test
+    public void testMergeWith_Blocking()
+    {
+        final ConnectionCountLimit first = () -> 10;
+        final ConnectionCountLimit second = ConnectionCountLimit.blockedUser();
+
+        ConnectionCountLimit limits = first.mergeWith(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(0), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+        limits = second.mergeWith(first);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(0), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+        limits = second.mergeWith(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(0), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+    }
+
+    @Test
+    public void testMergeWith_Empty()
+    {
+        final ConnectionCountLimit first = () -> 10;
+        final ConnectionCountLimit second = ConnectionCountLimit.blockedUser();
+        final ConnectionCountLimit empty = ConnectionCountLimit.noLimits();
+
+        assertTrue(empty.isEmpty());
+        assertFalse(empty.isUserBlocked());
+        assertNull(empty.getCountLimit());
+
+
+        ConnectionCountLimit limits = empty.mergeWith(first);
+        assertNotNull(limits);
+        assertEquals(first.getCountLimit(), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = first.mergeWith(empty);
+        assertNotNull(limits);
+        assertEquals(first.getCountLimit(), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+
+        limits = empty.mergeWith(second);
+        assertNotNull(limits);
+        assertEquals(second.getCountLimit(), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+        limits = second.mergeWith(empty);
+        assertNotNull(limits);
+        assertEquals(second.getCountLimit(), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+
+        limits = empty.mergeWith(empty);
+        assertTrue(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+        assertNull(limits.getCountLimit());
+    }
+
+    @Test
+    public void testMergeWith_Null()
+    {
+        final ConnectionCountLimit first = () -> 10;
+        final ConnectionCountLimit second = ConnectionCountLimit.blockedUser();
+        final ConnectionCountLimit empty = ConnectionCountLimit.noLimits();
+
+        assertEquals(first, first.mergeWith(null));
+        assertEquals(second, second.mergeWith(null));
+        assertEquals(empty, empty.mergeWith(null));
+    }
+
+    @Test
+    public void testThen_NonBlocking()
+    {
+        final ConnectionCountLimit first = () -> null;
+        final ConnectionCountLimit second = () -> 10;
+
+        ConnectionCountLimit limits = first.then(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(10), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = second.then(first);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(10), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+    }
+
+    @Test
+    public void testThen_Smaller()
+    {
+        final ConnectionCountLimit first = () -> 20;
+        final ConnectionCountLimit second = () -> 10;
+
+        ConnectionCountLimit limits = first.then(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(20), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = second.then(first);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(10), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+    }
+
+    @Test
+    public void testThen_Blocking()
+    {
+        final ConnectionCountLimit first = () -> 20;
+        final ConnectionCountLimit second = ConnectionCountLimit.blockedUser();
+
+        ConnectionCountLimit limits = first.then(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(20), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = second.then(first);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(0), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+        limits = second.mergeWith(ConnectionCountLimit.noLimits());
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(0), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+    }
+
+    @Test
+    public void testThen_Empty()
+    {
+        final ConnectionCountLimit first = () -> 20;
+        final ConnectionCountLimit second = ConnectionCountLimit.blockedUser();
+        final ConnectionCountLimit empty = ConnectionCountLimit.noLimits();
+        final ConnectionCountLimit empty2 = () -> null;
+
+        assertTrue(empty.isEmpty());
+        assertFalse(empty.isUserBlocked());
+        assertNull(empty.getCountLimit());
+
+        assertTrue(empty2.isEmpty());
+        assertFalse(empty2.isUserBlocked());
+        assertNull(empty2.getCountLimit());
+
+
+        ConnectionCountLimit limits = empty.then(first);
+        assertNotNull(limits);
+        assertEquals(first.getCountLimit(), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = empty2.then(first);
+        assertNotNull(limits);
+        assertEquals(first.getCountLimit(), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+
+        limits = empty.then(second);
+        assertNotNull(limits);
+        assertEquals(second.getCountLimit(), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+        limits = empty2.then(second);
+        assertNotNull(limits);
+        assertEquals(second.getCountLimit(), limits.getCountLimit());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+
+        limits = empty.then(empty);
+        assertTrue(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+        assertNull(limits.getCountLimit());
+
+        limits = empty2.then(empty);
+        assertTrue(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+        assertNull(limits.getCountLimit());
+    }
+
+    @Test
+    public void testThen_Null()
+    {
+        final ConnectionCountLimit first = () -> 20;
+        final ConnectionCountLimit second = ConnectionCountLimit.blockedUser();
+        final ConnectionCountLimit empty = ConnectionCountLimit.noLimits();
+
+        assertEquals(first, first.then(null));
+        assertEquals(second, second.then(null));
+        assertEquals(empty, empty.then(null));
+    }
+
+    @Test
+    public void testNewInstance()
+    {
+        final ConnectionCountLimit first = ConnectionCountLimit.newInstance(
+                new NonBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 50, 200, null));
+        assertNotNull(first);
+        assertEquals(Integer.valueOf(50), first.getCountLimit());
+        assertFalse(first.isUserBlocked());
+        assertFalse(first.isEmpty());
+
+        final ConnectionCountLimit second = ConnectionCountLimit.newInstance(
+                new NonBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, null, 200, null));
+        assertNotNull(second);
+        assertNull(second.getCountLimit());
+        assertFalse(second.isUserBlocked());
+        assertTrue(second.isEmpty());
+
+        final ConnectionCountLimit third = ConnectionCountLimit.newInstance(new BlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+        assertNotNull(third);
+        assertEquals(Integer.valueOf(0), third.getCountLimit());
+        assertTrue(third.isUserBlocked());
+        assertFalse(third.isEmpty());
+    }
+}
\ No newline at end of file
diff --git a/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/ConnectionLimitsTest.java b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/ConnectionLimitsTest.java
new file mode 100644
index 0000000..59f4674
--- /dev/null
+++ b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/ConnectionLimitsTest.java
@@ -0,0 +1,346 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.time.Duration;
+import java.util.Collections;
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.apache.qpid.test.utils.UnitTestBase;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class ConnectionLimitsTest extends UnitTestBase
+{
+    @Test
+    public void testMergeWith_NonBlocking()
+    {
+        final ConnectionLimits first = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 10, null, Duration.ofMinutes(1L));
+        final ConnectionLimits second = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, null, 300, Duration.ofMinutes(2L));
+
+        ConnectionLimits limits = first.mergeWith(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(10), limits.getCountLimit());
+        assertEquals(Collections.singletonMap(Duration.ofMinutes(2L), 300), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = second.mergeWith(first);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(10), limits.getCountLimit());
+        assertEquals(Collections.singletonMap(Duration.ofMinutes(2L), 300), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+    }
+
+    @Test
+    public void testMergeWith_Smaller()
+    {
+        final ConnectionLimits first = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 50, 200, Duration.ofMinutes(1L));
+        final ConnectionLimits second = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 20, 300, Duration.ofMinutes(1L));
+
+        ConnectionLimits limits = first.mergeWith(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(20), limits.getCountLimit());
+        assertEquals(Collections.singletonMap(Duration.ofMinutes(1L), 200), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = second.mergeWith(first);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(20), limits.getCountLimit());
+        assertEquals(Collections.singletonMap(Duration.ofMinutes(1L), 200), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+    }
+
+    @Test
+    public void testMergeWith_Blocking()
+    {
+        final ConnectionLimits first = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 10, 20, Duration.ofMinutes(7L));
+        final ConnectionLimits second = new BlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS);
+
+        ConnectionLimits limits = first.mergeWith(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(0), limits.getCountLimit());
+        assertTrue(limits.getFrequencyLimits().isEmpty());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+        limits = second.mergeWith(first);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(0), limits.getCountLimit());
+        assertTrue(limits.getFrequencyLimits().isEmpty());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+        limits = second.mergeWith(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(0), limits.getCountLimit());
+        assertTrue(limits.getFrequencyLimits().isEmpty());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+        limits = second.mergeWith(ConnectionLimits.noLimits());
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(0), limits.getCountLimit());
+        assertTrue(limits.getFrequencyLimits().isEmpty());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+    }
+
+    @Test
+    public void testMergeWith_Empty()
+    {
+        final ConnectionLimits first = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 10, 20, Duration.ofMinutes(3L));
+        final ConnectionLimits second = new BlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS);
+        final ConnectionLimits empty = ConnectionLimits.noLimits();
+
+        assertTrue(empty.isEmpty());
+        assertFalse(empty.isUserBlocked());
+        assertNull(empty.getCountLimit());
+        assertTrue(empty.getFrequencyLimits().isEmpty());
+
+
+        ConnectionLimits limits = empty.mergeWith(first);
+        assertNotNull(limits);
+        assertEquals(first.getCountLimit(), limits.getCountLimit());
+        assertEquals(first.getFrequencyLimits(), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = first.mergeWith(empty);
+        assertNotNull(limits);
+        assertEquals(first.getCountLimit(), limits.getCountLimit());
+        assertEquals(first.getFrequencyLimits(), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+
+        limits = empty.mergeWith(second);
+        assertNotNull(limits);
+        assertEquals(second.getCountLimit(), limits.getCountLimit());
+        assertEquals(second.getFrequencyLimits(), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+        limits = second.mergeWith(empty);
+        assertNotNull(limits);
+        assertEquals(second.getCountLimit(), limits.getCountLimit());
+        assertEquals(second.getFrequencyLimits(), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+
+        limits = empty.mergeWith(empty);
+        assertTrue(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+        assertNull(limits.getCountLimit());
+        assertTrue(limits.getFrequencyLimits().isEmpty());
+    }
+
+    @Test
+    public void testMergeWith_Null()
+    {
+        final ConnectionLimits first = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 10, 20, Duration.ofMinutes(1L));
+        final ConnectionLimits second = new BlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS);
+        final ConnectionLimits empty = ConnectionLimits.noLimits();
+
+        assertEquals(first, first.mergeWith(null));
+        assertEquals(second, second.mergeWith(null));
+        assertEquals(empty, empty.mergeWith(null));
+    }
+
+    @Test
+    public void testThen_NonBlocking()
+    {
+        final ConnectionLimits first = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 20, null, Duration.ofMinutes(2L));
+        final ConnectionLimits second = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, null, 300, Duration.ofMinutes(2L));
+
+        ConnectionLimits limits = first.then(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(20), limits.getCountLimit());
+        assertTrue(limits.getFrequencyLimits().isEmpty());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = second.then(first);
+        assertNotNull(limits);
+        assertNull(limits.getCountLimit());
+        assertEquals(Collections.singletonMap(Duration.ofMinutes(2L), 300), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+    }
+
+    @Test
+    public void testThen_Smaller()
+    {
+        final ConnectionLimits first = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 50, 200, Duration.ofMinutes(2L));
+        final ConnectionLimits second = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 20, 30, Duration.ofMinutes(3L));
+
+        ConnectionLimits limits = first.then(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(50), limits.getCountLimit());
+        assertEquals(Collections.singletonMap(Duration.ofMinutes(2L), 200), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = second.then(first);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(20), limits.getCountLimit());
+        assertEquals(Collections.singletonMap(Duration.ofMinutes(3L), 30), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+    }
+
+    @Test
+    public void testThen_Blocking()
+    {
+        final ConnectionLimits first = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 10, null, null);
+        final ConnectionLimits second = new BlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS);
+
+        ConnectionLimits limits = first.then(second);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(10), limits.getCountLimit());
+        assertTrue(limits.getFrequencyLimits().isEmpty());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = second.then(first);
+        assertNotNull(limits);
+        assertEquals(Integer.valueOf(0), limits.getCountLimit());
+        assertTrue(limits.getFrequencyLimits().isEmpty());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+    }
+
+    @Test
+    public void testThen_Empty()
+    {
+        final ConnectionLimits first = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 10, 20, Duration.ofHours(1L));
+        final ConnectionLimits second = new BlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS);
+        final ConnectionLimits empty = ConnectionLimits.noLimits();
+        final ConnectionLimits empty2 = new ConnectionLimits()
+        {
+            @Override
+            public Integer getCountLimit()
+            {
+                return null;
+            }
+
+            @Override
+            public Map<Duration, Integer> getFrequencyLimits()
+            {
+                return Collections.emptyMap();
+            }
+
+            @Override
+            public boolean isUserBlocked()
+            {
+                return false;
+            }
+
+            @Override
+            public boolean isEmpty()
+            {
+                return true;
+            }
+        };
+
+        assertTrue(empty.isEmpty());
+        assertFalse(empty.isUserBlocked());
+        assertNull(empty.getCountLimit());
+        assertTrue(empty.getFrequencyLimits().isEmpty());
+
+
+        ConnectionLimits limits = empty.then(first);
+        assertNotNull(limits);
+        assertEquals(first.getCountLimit(), limits.getCountLimit());
+        assertEquals(first.getFrequencyLimits(), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+        limits = empty2.then(first);
+        assertNotNull(limits);
+        assertEquals(first.getCountLimit(), limits.getCountLimit());
+        assertEquals(first.getFrequencyLimits(), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+
+
+        limits = empty.then(second);
+        assertNotNull(limits);
+        assertEquals(second.getCountLimit(), limits.getCountLimit());
+        assertEquals(second.getFrequencyLimits(), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+        limits = empty2.then(second);
+        assertNotNull(limits);
+        assertEquals(second.getCountLimit(), limits.getCountLimit());
+        assertEquals(second.getFrequencyLimits(), limits.getFrequencyLimits());
+        assertFalse(limits.isEmpty());
+        assertTrue(limits.isUserBlocked());
+
+
+        limits = empty.then(empty);
+        assertTrue(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+        assertNull(limits.getCountLimit());
+        assertTrue(limits.getFrequencyLimits().isEmpty());
+
+        limits = empty2.then(empty);
+        assertTrue(limits.isEmpty());
+        assertFalse(limits.isUserBlocked());
+        assertNull(limits.getCountLimit());
+        assertTrue(limits.getFrequencyLimits().isEmpty());
+    }
+
+    @Test
+    public void testThen_Null()
+    {
+        final ConnectionLimits first = new NonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 10, 20, Duration.ofMinutes(17L));
+        final ConnectionLimits second = new BlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS);
+        final ConnectionLimits empty = ConnectionLimits.noLimits();
+
+        assertEquals(first, first.then(null));
+        assertEquals(second, second.then(null));
+        assertEquals(empty, empty.then(null));
+    }
+}
\ No newline at end of file
diff --git a/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/FileParserTest.java b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/FileParserTest.java
new file mode 100644
index 0000000..9f45a32
--- /dev/null
+++ b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/FileParserTest.java
@@ -0,0 +1,522 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Collections;
+
+import com.google.common.collect.Iterables;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import org.apache.qpid.server.configuration.IllegalConfigurationException;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class FileParserTest extends UnitTestBase
+{
+    private static final String FILE_SUFFIX = "clt";
+    private static final Charset CHARSET = StandardCharsets.UTF_8;
+
+    @Test
+    public void testParseRule_ConnectionCountLimit()
+    {
+        final RuleSetCreator creator = writeConfig("CLT user port=amqp connection_limit=10");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals("amqp", rule.getPort());
+        assertFalse(rule.isUserBlocked());
+        assertEquals(Integer.valueOf(10), rule.getCountLimit());
+        assertNotNull(rule.getFrequencyLimits());
+        assertNull(rule.getFrequencyPeriod());
+        assertNull(rule.getFrequencyLimit());
+        assertTrue(rule.getFrequencyLimits().isEmpty());
+    }
+
+    @Test
+    public void testParseRule_ConnectionFrequencyLimit()
+    {
+        final RuleSetCreator creator = writeConfig("CLT user port=amqp connection-frequency-limit=10/15M");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals("amqp", rule.getPort());
+        assertFalse(rule.isUserBlocked());
+        assertEquals(Collections.singletonMap(Duration.ofMinutes(15L), 10), rule.getFrequencyLimits());
+        assertEquals(Duration.ofMinutes(15L), rule.getFrequencyPeriod());
+        assertEquals(Integer.valueOf(10), rule.getFrequencyLimit());
+        assertNull(rule.getCountLimit());
+    }
+
+    @Test
+    public void testParseRule_ConnectionFrequencyLimit_1minute()
+    {
+        final RuleSetCreator creator = writeConfig("CLT user port=amqp connection-frequency-limit=17/m");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals("amqp", rule.getPort());
+        assertFalse(rule.isUserBlocked());
+        assertEquals(Collections.singletonMap(Duration.ofMinutes(1L), 17), rule.getFrequencyLimits());
+        assertEquals(Duration.ofMinutes(1L), rule.getFrequencyPeriod());
+        assertEquals(Integer.valueOf(17), rule.getFrequencyLimit());
+        assertNull(rule.getCountLimit());
+    }
+
+    @Test
+    public void testParseRule_ConnectionFrequencyLimit_genericPeriod()
+    {
+        final RuleSetCreator creator = writeConfig("CLT user port=amqp connection-frequency-limit=17/P1dT1m17.7s");
+        final Duration duration = Duration.parse("P1dT1m17.7s");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals("amqp", rule.getPort());
+        assertFalse(rule.isUserBlocked());
+
+        assertEquals(Collections.singletonMap(duration, 17), rule.getFrequencyLimits());
+        assertEquals(duration, rule.getFrequencyPeriod());
+        assertEquals(Integer.valueOf(17), rule.getFrequencyLimit());
+        assertNull(rule.getCountLimit());
+    }
+
+    @Test
+    public void testParseRule_ConnectionFrequencyLimit_noPeriod()
+    {
+        final RuleSetCreator creator = writeConfig("CLT user port=amqp connection-frequency-limit=17");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals("amqp", rule.getPort());
+        assertFalse(rule.isUserBlocked());
+
+        assertNull(rule.getFrequencyPeriod());
+        assertEquals(Integer.valueOf(17), rule.getFrequencyLimit());
+        assertNull(rule.getCountLimit());
+    }
+
+    @Test
+    public void testParseRule_ConnectionLimit()
+    {
+        final RuleSetCreator creator = writeConfig("CLT user port=amqp connection_limit=10 connection-frequency-limit=20/m");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals("amqp", rule.getPort());
+        assertFalse(rule.isUserBlocked());
+        assertEquals(Integer.valueOf(10), rule.getCountLimit());
+        assertEquals(Integer.valueOf(20), rule.getFrequencyLimit());
+        assertEquals(Duration.ofMinutes(1L), rule.getFrequencyPeriod());
+        assertEquals(Collections.singletonMap(Duration.ofMinutes(1L), 20), rule.getFrequencyLimits());
+    }
+
+    @Test
+    public void testParseRule_ConnectionLimit2()
+    {
+        final RuleSetCreator creator = writeConfig("CLT\tuser\tport='amqp' connection_limit=\"10\" connection-frequency-limit='20/h'");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals("amqp", rule.getPort());
+        assertFalse(rule.isUserBlocked());
+        assertEquals(Integer.valueOf(10), rule.getCountLimit());
+        assertEquals(Integer.valueOf(20), rule.getFrequencyLimit());
+        assertEquals(Duration.ofHours(1L), rule.getFrequencyPeriod());
+        assertEquals(Collections.singletonMap(Duration.ofHours(1L), 20), rule.getFrequencyLimits());
+    }
+
+    @Test
+    public void testParseRule_Continuation()
+    {
+        final RuleSetCreator creator = writeConfig("CLT user \\ \n connectionLimit=10 connection-frequency-limit=20");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals(RulePredicates.ALL_PORTS, rule.getPort());
+        assertFalse(rule.isUserBlocked());
+        assertEquals(Integer.valueOf(10), rule.getCountLimit());
+        assertEquals(Integer.valueOf(20), rule.getFrequencyLimit());
+    }
+
+    @Test
+    public void testParseRule_Blocked()
+    {
+        final RuleSetCreator creator = writeConfig("CLT user block port=amqp");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals("amqp", rule.getPort());
+        assertTrue(rule.isUserBlocked());
+        assertEquals(Integer.valueOf(0), rule.getCountLimit());
+        assertEquals(Integer.valueOf(0), rule.getFrequencyLimit());
+        assertNotNull(rule.getFrequencyLimits());
+        assertTrue(rule.getFrequencyLimits().isEmpty());
+    }
+
+    @Test
+    public void testParseRule_DefaultPort()
+    {
+        final RuleSetCreator creator = writeConfig("CLT user connection_limit=10 connection-frequency-limit=20");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals(RulePredicates.ALL_PORTS, rule.getPort());
+        assertFalse(rule.isUserBlocked());
+        assertEquals(Integer.valueOf(10), rule.getCountLimit());
+        assertEquals(Integer.valueOf(20), rule.getFrequencyLimit());
+    }
+
+    @Test
+    public void testParseRule_Empty()
+    {
+        final RuleSetCreator creator = writeConfig("CLT user port=all");
+        assertTrue(creator.isEmpty());
+    }
+
+    @Test
+    public void testParseConfig()
+    {
+        final RuleSetCreator creator = writeConfig("CONFIG log-all=true default-frequency-period=200000");
+        assertTrue(creator.isEmpty());
+        assertTrue(creator.isLogAllMessages());
+        assertEquals(Long.valueOf(200000L), creator.getDefaultFrequencyPeriod());
+    }
+
+    @Test
+    public void testParseConfig_alternative()
+    {
+        final RuleSetCreator creator = writeConfig("CONFIG logAll=true defaultFrequencyPeriod=200000");
+        assertTrue(creator.isEmpty());
+        assertTrue(creator.isLogAllMessages());
+        assertEquals(Long.valueOf(200000L), creator.getDefaultFrequencyPeriod());
+    }
+
+    @Test
+    public void testParseConfig_Empty()
+    {
+        final RuleSetCreator creator = writeConfig("CONFIG log-all=false");
+        assertTrue(creator.isEmpty());
+        assertFalse(creator.isLogAllMessages());
+        assertNull(creator.getDefaultFrequencyPeriod());
+    }
+
+    @Test
+    public void testParseAclRule_ConnectionCountLimit()
+    {
+        final RuleSetCreator creator = writeConfig("ACL ALLOW-LOG user ACCESS VIRTUALHOST connection_limit=10");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals(RulePredicates.ALL_PORTS, rule.getPort());
+        assertFalse(rule.isUserBlocked());
+        assertEquals(Integer.valueOf(10), rule.getCountLimit());
+        assertNull(rule.getFrequencyLimit());
+        assertNotNull(rule.getFrequencyLimits());
+        assertTrue(rule.getFrequencyLimits().isEmpty());
+    }
+
+    @Test
+    public void testParseAclRule_ConnectionFrequencyLimit()
+    {
+        final RuleSetCreator creator = writeConfig("ACL ALLOW-LOG user ACCESS VIRTUALHOST connection_frequency_limit=60");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals(RulePredicates.ALL_PORTS, rule.getPort());
+        assertFalse(rule.isUserBlocked());
+        assertEquals(Integer.valueOf(60), rule.getFrequencyLimit());
+        assertNull(rule.getCountLimit());
+    }
+
+    @Test
+    public void testParseAclRule_ConnectionLimit()
+    {
+        final RuleSetCreator creator = writeConfig("ACL ALLOW-LOG user ACCESS VIRTUALHOST connection_limit=10 connection_frequency_limit=60");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals(RulePredicates.ALL_PORTS, rule.getPort());
+        assertFalse(rule.isUserBlocked());
+        assertEquals(Integer.valueOf(60), rule.getFrequencyLimit());
+        assertEquals(Integer.valueOf(10), rule.getCountLimit());
+    }
+
+    @Test
+    public void testParseAclRule_ConnectionLimit2()
+    {
+        final RuleSetCreator creator = writeConfig("27 ACL ALLOW-LOG user ACCESS VIRTUALHOST connection_limit=10 connection_frequency_limit=60");
+
+        final Rule rule = Iterables.getFirst(creator, null);
+        assertNotNull(rule);
+        assertEquals("user", rule.getIdentity());
+        assertEquals(RulePredicates.ALL_PORTS, rule.getPort());
+        assertFalse(rule.isUserBlocked());
+        assertEquals(Integer.valueOf(60), rule.getFrequencyLimit());
+        assertEquals(Integer.valueOf(10), rule.getCountLimit());
+    }
+
+    @Test
+    public void testParseAclRule_Empty()
+    {
+        final RuleSetCreator creator = writeConfig("ACL ALLOW-LOG user ACCESS VIRTUALHOST name=vhost");
+        assertTrue(creator.isEmpty());
+    }
+
+    @Test
+    public void testParseAclRule_Empty2()
+    {
+        final RuleSetCreator creator = writeConfig("ACL DENY user ACCESS");
+        assertTrue(creator.isEmpty());
+    }
+
+    @Test
+    public void testParse_Error_Continuation()
+    {
+        try
+        {
+            writeConfig("CLT user \\ connection_limit=10 connection_frequency_limit=60");
+            fail("An exception is expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            assertEquals(String.format(FileParser.PREMATURE_CONTINUATION, 1), e.getMessage());
+        }
+    }
+
+    @Test
+    public void testParse_Error_NotEnoughTokens()
+    {
+        try
+        {
+            writeConfig("CLT");
+            fail("An exception is expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            assertEquals(String.format(FileParser.NOT_ENOUGH_TOKENS, 1), e.getMessage());
+        }
+    }
+
+    @Test
+    public void testParse_Error_UnknownTokens()
+    {
+        try
+        {
+            writeConfig("CLT user connection_limit=10 connection_frequency_limit=60 name=vhost");
+            fail("An exception is expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            assertEquals(String.format(FileParser.UNKNOWN_CLT_PROPERTY_MSG, "name", 1), e.getMessage());
+        }
+    }
+
+    @Test
+    public void testParse_Error_OrderNumber()
+    {
+        try
+        {
+            writeConfig("7 CLT user connection_limit=10 connection_frequency_limit=60");
+            fail("An exception is expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            assertEquals(String.format(FileParser.NUMBER_NOT_ALLOWED, "CLT", 1), e.getMessage());
+        }
+    }
+
+    @Test
+    public void testParse_Error_NumberFormat()
+    {
+        try
+        {
+            writeConfig("CLT user connection_limit=10 connection_frequency_limit=xc");
+            fail("An exception is expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            assertEquals(String.format(FileParser.PARSE_TOKEN_FAILED, 1), e.getMessage());
+        }
+    }
+
+    @Test
+    public void testParse_Error_MissingEqualSign()
+    {
+        try
+        {
+            writeConfig("CLT user connection_limit=10 connection_frequency_limit,60");
+            fail("An exception is expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            assertEquals(String.format(FileParser.PROPERTY_NO_EQUALS_MSG, 1), e.getMessage());
+        }
+    }
+
+    @Test
+    public void testParse_Error_MissingValue()
+    {
+        try
+        {
+            writeConfig("CLT user connection_limit=10 connection_frequency_limit=");
+            fail("An exception is expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            assertEquals(String.format(FileParser.PROPERTY_NO_VALUE_MSG, 1), e.getMessage());
+        }
+    }
+
+    @Test
+    public void testParse_Error_KeyOnly()
+    {
+        try
+        {
+            writeConfig("CLT user connection_limit=10 connection_frequency_limit");
+            fail("An exception is expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            assertEquals(String.format(FileParser.PROPERTY_KEY_ONLY_MSG, 1), e.getMessage());
+        }
+    }
+
+    @Test
+    public void testParse_Error_UnknownToken()
+    {
+        try
+        {
+            writeConfig("GROUP group user");
+            fail("An exception is expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            assertEquals(String.format(FileParser.UNRECOGNISED_INITIAL_TOKEN, "GROUP", 1), e.getMessage());
+        }
+    }
+
+    @Test
+    public void testParse_Error_UnknownFile()
+    {
+        final String prefix = getClass().getSimpleName() + "." + getTestName();
+        final Path connecionLimitFile;
+        try
+        {
+            connecionLimitFile = Files.createTempFile(prefix, FILE_SUFFIX);
+            Files.deleteIfExists(connecionLimitFile);
+        }
+        catch (IOException e)
+        {
+            throw new IllegalStateException("Failed to create a file: " + prefix + FILE_SUFFIX);
+        }
+
+        try
+        {
+            FileParser.parse(connecionLimitFile.toAbsolutePath().toString());
+            fail("An exception is expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+    }
+
+    @Test
+    public void testParse_IOException() throws IOException
+    {
+        Reader reader = Mockito.mock(Reader.class);
+
+        Mockito.doThrow(new IOException("exception")).when(reader).reset();
+        Mockito.doThrow(new IOException("exception")).when(reader).ready();
+        Mockito.doThrow(new IOException("exception")).when(reader).close();
+        Mockito.doThrow(new IOException("exception")).when(reader).read(Mockito.any(CharBuffer.class));
+        Mockito.doThrow(new IOException("exception")).when(reader).read(Mockito.any(char[].class));
+        Mockito.doThrow(new IOException("exception")).when(reader).read(Mockito.any(char[].class), Mockito.anyInt(), Mockito.anyInt());
+        Mockito.doThrow(new IOException("exception")).when(reader).mark(Mockito.anyInt());
+        Mockito.doThrow(new IOException("exception")).when(reader).skip(Mockito.anyLong());
+
+        try
+        {
+            new FileParser().readAndParse(reader);
+            fail("An exception is expected");
+        }
+        catch (IllegalConfigurationException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+    }
+
+    private RuleSetCreator writeConfig(String... data)
+    {
+        final String prefix = getClass().getSimpleName() + "." + getTestName();
+        final Path connectionLimitFile;
+        try
+        {
+            connectionLimitFile = Files.createTempFile(prefix, FILE_SUFFIX);
+            Files.deleteIfExists(connectionLimitFile);
+        }
+        catch (IOException e)
+        {
+            throw new IllegalStateException("Failed to create a file: " + prefix + FILE_SUFFIX);
+        }
+
+        try (BufferedWriter writer = Files.newBufferedWriter(connectionLimitFile, CHARSET))
+        {
+            for (String line : data)
+            {
+                writer.write(line);
+                writer.newLine();
+                writer.flush();
+            }
+        }
+        catch (IOException x)
+        {
+            throw new IllegalStateException("Failed to write into the file " + connectionLimitFile.getFileName());
+        }
+        return FileParser.parse(connectionLimitFile.toAbsolutePath().toString());
+    }
+}
\ No newline at end of file
diff --git a/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/RulePredicatesTest.java b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/RulePredicatesTest.java
new file mode 100644
index 0000000..6e505e5
--- /dev/null
+++ b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/RulePredicatesTest.java
@@ -0,0 +1,229 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.time.Duration;
+
+import org.junit.Test;
+
+import org.apache.qpid.server.user.connection.limits.config.RulePredicates.Property;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class RulePredicatesTest extends UnitTestBase
+{
+    @Test
+    public void testEmpty()
+    {
+        final RulePredicates predicates = new RulePredicates();
+        predicates.parse("Unknown", "23");
+
+        assertFalse(predicates.isUserBlocked());
+        assertNull(predicates.getConnectionCountLimit());
+        assertNull(predicates.getConnectionFrequencyLimit());
+        assertEquals(RulePredicates.ALL_PORTS, predicates.getPort());
+        assertTrue(predicates.isEmpty());
+    }
+
+    @Test
+    public void testParse_port()
+    {
+        final RulePredicates predicates = new RulePredicates();
+        assertEquals(RulePredicates.ALL_PORTS, predicates.getPort());
+
+        predicates.parse(Property.PORT.name(), "amqp");
+        assertEquals("amqp", predicates.getPort());
+
+        try
+        {
+            predicates.parse(Property.PORT.name(), "amqps");
+            fail("An exception is expected here, multiple ports");
+        }
+        catch (IllegalStateException e)
+        {
+            assertEquals("Property 'port' has already been defined", e.getMessage());
+        }
+    }
+
+    @Test
+    public void testParse_connectionLimit()
+    {
+        final RulePredicates predicates = new RulePredicates();
+        assertNull(predicates.getConnectionCountLimit());
+
+        predicates.parse(Property.CONNECTION_LIMIT.name(), "70");
+        assertEquals(Integer.valueOf(70), predicates.getConnectionCountLimit());
+
+        try
+        {
+            predicates.parse(Property.CONNECTION_LIMIT.name(), "75");
+            fail("An exception is expected here, multiple connection count limits");
+        }
+        catch (IllegalStateException e)
+        {
+            assertEquals("Property 'connection_limit' has already been defined", e.getMessage());
+        }
+    }
+
+    @Test
+    public void testParse_connectionLimit_negative()
+    {
+        final RulePredicates predicates = new RulePredicates();
+        assertNull(predicates.getConnectionCountLimit());
+
+        predicates.parse(Property.CONNECTION_LIMIT.name(), "-1");
+        assertEquals(Integer.valueOf(Integer.MAX_VALUE), predicates.getConnectionCountLimit());
+    }
+
+    @Test
+    public void testParse_frequencyLimit()
+    {
+        final RulePredicates predicates = new RulePredicates();
+        assertNull(predicates.getConnectionFrequencyLimit());
+        assertNull(predicates.getConnectionFrequencyPeriod());
+
+        predicates.parse(Property.CONNECTION_FREQUENCY_LIMIT.name(), "70");
+        assertEquals(Integer.valueOf(70), predicates.getConnectionFrequencyLimit());
+        assertNull(predicates.getConnectionFrequencyPeriod());
+
+        try
+        {
+            predicates.parse(Property.CONNECTION_FREQUENCY_LIMIT.name(), "75");
+            fail("An exception is expected here, multiple connection frequency limits");
+        }
+        catch (IllegalStateException e)
+        {
+            assertEquals("Property 'connection_frequency_limit' has already been defined", e.getMessage());
+        }
+    }
+
+    @Test
+    public void testParse_frequencyLimitWithTimePeriod()
+    {
+        RulePredicates predicates = new RulePredicates();
+        predicates.parse(Property.CONNECTION_FREQUENCY_LIMIT.name(), "70/23m");
+        assertEquals(Integer.valueOf(70), predicates.getConnectionFrequencyLimit());
+        assertEquals(Duration.ofMinutes(23L), predicates.getConnectionFrequencyPeriod());
+
+        predicates = new RulePredicates();
+        predicates.parse(Property.CONNECTION_FREQUENCY_LIMIT.name(), "67/H");
+        assertEquals(Integer.valueOf(67), predicates.getConnectionFrequencyLimit());
+        assertEquals(Duration.ofHours(1L), predicates.getConnectionFrequencyPeriod());
+
+        predicates = new RulePredicates();
+        predicates.parse(Property.CONNECTION_FREQUENCY_LIMIT.name(), "11/PT5m7.5s");
+        assertEquals(Integer.valueOf(11), predicates.getConnectionFrequencyLimit());
+        assertEquals(Duration.parse("PT5m7.5s"), predicates.getConnectionFrequencyPeriod());
+    }
+
+    @Test
+    public void testParse_frequencyLimitWithTimePeriod_longDescription()
+    {
+        RulePredicates predicates = new RulePredicates();
+        predicates.parse(Property.CONNECTION_FREQUENCY_LIMIT.name(), "70/Second");
+        assertEquals(Integer.valueOf(70), predicates.getConnectionFrequencyLimit());
+        assertEquals(Duration.ofSeconds(1L), predicates.getConnectionFrequencyPeriod());
+
+        predicates = new RulePredicates();
+        predicates.parse(Property.CONNECTION_FREQUENCY_LIMIT.name(), "67/Minute");
+        assertEquals(Integer.valueOf(67), predicates.getConnectionFrequencyLimit());
+        assertEquals(Duration.ofMinutes(1L), predicates.getConnectionFrequencyPeriod());
+
+        predicates = new RulePredicates();
+        predicates.parse(Property.CONNECTION_FREQUENCY_LIMIT.name(), "11/Hour");
+        assertEquals(Integer.valueOf(11), predicates.getConnectionFrequencyLimit());
+        assertEquals(Duration.ofHours(1L), predicates.getConnectionFrequencyPeriod());
+    }
+
+    @Test
+    public void testParse_frequencyLimit_negative()
+    {
+        final RulePredicates predicates = new RulePredicates();
+        assertNull(predicates.getConnectionFrequencyLimit());
+
+        predicates.parse(Property.CONNECTION_FREQUENCY_LIMIT.name(), "-1");
+        assertEquals(Integer.valueOf(Integer.MAX_VALUE), predicates.getConnectionFrequencyLimit());
+    }
+
+    @Test
+    public void testParse_frequencyLimit_negativeTimePeriod()
+    {
+        final RulePredicates predicates = new RulePredicates();
+        assertNull(predicates.getConnectionFrequencyLimit());
+
+        try
+        {
+            predicates.parse(Property.CONNECTION_FREQUENCY_LIMIT.name(), "100/-PT2m");
+            fail("An exception is expected, time period can not be negative");
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertEquals("Frequency period can not be negative -120 s", e.getMessage());
+        }
+    }
+
+    @Test
+    public void testSetBlockedUser()
+    {
+        final RulePredicates predicates = new RulePredicates();
+        assertFalse(predicates.isUserBlocked());
+
+        predicates.setBlockedUser();
+        assertTrue(predicates.isUserBlocked());
+    }
+
+    @Test
+    public void testParseProperty()
+    {
+        assertEquals(Property.CONNECTION_LIMIT, RulePredicates.Property.parse("CONNECTION_LIMIT"));
+        assertEquals(Property.CONNECTION_LIMIT, RulePredicates.Property.parse("connection-limit"));
+        assertEquals(Property.CONNECTION_LIMIT, RulePredicates.Property.parse("ConnectionLimit"));
+        assertEquals(Property.CONNECTION_LIMIT, RulePredicates.Property.parse("ConNectioN-limit"));
+        assertNull(RulePredicates.Property.parse("Connection=Limit"));
+    }
+
+    @Test
+    public void testIsAllUser()
+    {
+        assertTrue(RulePredicates.isAllUser("all"));
+        assertTrue(RulePredicates.isAllUser("All"));
+        assertTrue(RulePredicates.isAllUser("ALL"));
+        assertFalse(RulePredicates.isAllUser("any"));
+        assertFalse(RulePredicates.isAllUser("Any"));
+        assertFalse(RulePredicates.isAllUser("ANY"));
+        assertFalse(RulePredicates.isAllUser("true"));
+    }
+
+    @Test
+    public void testIsAllPort()
+    {
+        assertTrue(RulePredicates.isAllPort("all"));
+        assertTrue(RulePredicates.isAllPort("All"));
+        assertTrue(RulePredicates.isAllPort("ALL"));
+        assertFalse(RulePredicates.isAllPort("any"));
+        assertFalse(RulePredicates.isAllPort("Any"));
+        assertFalse(RulePredicates.isAllPort("ANY"));
+        assertFalse(RulePredicates.isAllPort("true"));
+    }
+}
\ No newline at end of file
diff --git a/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/RuleSetCreatorTest.java b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/RuleSetCreatorTest.java
new file mode 100644
index 0000000..035e191
--- /dev/null
+++ b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/RuleSetCreatorTest.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.Test;
+
+import org.apache.qpid.test.utils.UnitTestBase;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class RuleSetCreatorTest extends UnitTestBase
+{
+    @Test
+    public void testAddRule()
+    {
+        final Rule rule1 = Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS);
+        final Rule rule2 = Rule.newNonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 20, 60, Duration.ofMinutes(1L));
+        final RuleSetCreator creator = new RuleSetCreator();
+
+        assertTrue(creator.add(rule1));
+        assertTrue(creator.addAll(Collections.singleton(rule2)));
+        assertFalse(creator.isEmpty());
+        assertEquals(2, creator.size());
+        assertTrue(creator.contains(rule1));
+        assertTrue(creator.contains(rule2));
+        assertTrue(creator.containsAll(Arrays.asList(rule1, rule2)));
+    }
+
+    @Test
+    public void testAddRule_null()
+    {
+        final RuleSetCreator creator = new RuleSetCreator();
+
+        assertFalse(creator.add(null));
+        assertFalse(creator.addAll(null));
+        assertTrue(creator.isEmpty());
+    }
+
+    @Test
+    public void testRemoveRule()
+    {
+        final Rule rule1 = Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS);
+        final Rule rule2 = Rule.newNonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 20, 60, Duration.ofMinutes(3L));
+        final RuleSetCreator creator = new RuleSetCreator();
+
+        assertTrue(creator.add(rule1));
+        assertTrue(creator.add(rule2));
+        assertFalse(creator.isEmpty());
+        assertEquals(2, creator.size());
+
+        assertTrue(creator.remove(rule1));
+        assertEquals(1, creator.size());
+        for (final Rule r : creator)
+        {
+            assertSame(rule2, r);
+        }
+        assertTrue(creator.removeAll(Collections.singleton(rule2)));
+        assertTrue(creator.isEmpty());
+    }
+
+    @Test
+    public void testClear()
+    {
+        final Rule rule1 = Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS);
+        final Rule rule2 = Rule.newNonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 20, 60, Duration.ofMinutes(2L));
+        final RuleSetCreator creator = new RuleSetCreator();
+
+        assertTrue(creator.add(rule1));
+        assertTrue(creator.add(rule2));
+        assertEquals(2, creator.size());
+
+        creator.clear();
+        assertTrue(creator.isEmpty());
+    }
+
+    @Test
+    public void testRetainAll()
+    {
+        final Rule rule1 = Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS);
+        final Rule rule2 = Rule.newNonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 20, 60, Duration.ofMinutes(2L));
+        final RuleSetCreator creator = new RuleSetCreator();
+
+        assertTrue(creator.add(rule1));
+        assertTrue(creator.add(rule2));
+        assertFalse(creator.isEmpty());
+        assertEquals(2, creator.size());
+
+        assertTrue(creator.retainAll(Collections.singleton(rule2)));
+        assertEquals(1, creator.size());
+        for (final Rule r : creator)
+        {
+            assertSame(rule2, r);
+        }
+    }
+
+    @Test
+    public void testDefaultFrequencyPeriod()
+    {
+        final RuleSetCreator creator = new RuleSetCreator();
+        assertNull(creator.getDefaultFrequencyPeriod());
+        assertFalse(creator.isDefaultFrequencyPeriodSet());
+
+        creator.setDefaultFrequencyPeriod(30L);
+        assertEquals(Long.valueOf(30L), creator.getDefaultFrequencyPeriod());
+        assertTrue(creator.isDefaultFrequencyPeriodSet());
+    }
+
+    @Test
+    public void testLogAllMessages()
+    {
+        final RuleSetCreator creator = new RuleSetCreator();
+        assertFalse(creator.isLogAllMessages());
+
+        creator.setLogAllMessages(true);
+        assertTrue(creator.isLogAllMessages());
+
+        creator.setLogAllMessages(false);
+        assertFalse(creator.isLogAllMessages());
+    }
+
+    @Test
+    public void testGetLimiter()
+    {
+        final RuleSetCreator creator = new RuleSetCreator();
+        final Rule rule = Rule.newNonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 20, 60, null);
+        creator.add(rule);
+
+        creator.setDefaultFrequencyPeriod(-1L);
+        assertNotNull(creator.getLimiter("Limiter"));
+        assertNull(rule.getFrequencyPeriod());
+
+        creator.setDefaultFrequencyPeriod(0L);
+        assertNotNull(creator.getLimiter("Limiter"));
+        assertNull(rule.getFrequencyPeriod());
+
+        creator.setDefaultFrequencyPeriod(400000L);
+        assertNotNull(creator.getLimiter("Limiter"));
+        assertEquals(Duration.ofMillis(400000L), rule.getFrequencyPeriod());
+    }
+
+    @Test
+    public void testUpdateRulesWithDefaultFrequencyPeriod()
+    {
+        final RuleSetCreator creator = new RuleSetCreator();
+        final Rule rule = Rule.newNonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 20, 60, null);
+        creator.add(rule);
+
+        creator.updateRulesWithDefaultFrequencyPeriod();
+        assertNull(rule.getFrequencyPeriod());
+
+        creator.setDefaultFrequencyPeriod(-1L);
+        creator.updateRulesWithDefaultFrequencyPeriod();
+        assertNull(rule.getFrequencyPeriod());
+
+        creator.setDefaultFrequencyPeriod(400000L);
+        creator.updateRulesWithDefaultFrequencyPeriod();
+        assertEquals(Duration.ofMillis(400000L), rule.getFrequencyPeriod());
+    }
+}
\ No newline at end of file
diff --git a/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/RuleSetTest.java b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/RuleSetTest.java
new file mode 100644
index 0000000..8b935b6
--- /dev/null
+++ b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/RuleSetTest.java
@@ -0,0 +1,1000 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.security.Principal;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Pattern;
+
+import javax.security.auth.Subject;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import org.apache.qpid.server.logging.EventLogger;
+import org.apache.qpid.server.model.port.AmqpPort;
+import org.apache.qpid.server.security.auth.AuthenticatedPrincipal;
+import org.apache.qpid.server.security.auth.TestPrincipalUtils;
+import org.apache.qpid.server.security.limit.ConnectionLimitException;
+import org.apache.qpid.server.security.limit.ConnectionSlot;
+import org.apache.qpid.server.transport.AMQPConnection;
+import org.apache.qpid.server.user.connection.limits.config.RuleSet.Builder;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class RuleSetTest extends UnitTestBase
+{
+    private static final String TEST_USER = "user";
+
+    private static final String OTHER_USER = "other";
+
+    private static final String TEST_GROUP1 = "group1";
+    private static final String TEST_GROUP2 = "group2";
+
+    private static final String OTHER_GROUP = "anotherGroup";
+
+    private static final String TEST_PORT = "amqp";
+    private static final String LIMITER_NAME = "Limiter";
+
+    private EventLogger _eventLogger;
+
+    private AmqpPort<?> _port;
+
+    private Subject _subject;
+
+    private Principal _principal;
+
+    @Before
+    public void setUp()
+    {
+        _eventLogger = Mockito.mock(EventLogger.class);
+
+        _subject = TestPrincipalUtils.createTestSubject(TEST_USER, TEST_GROUP1, TEST_GROUP2);
+        for (Principal principal : _subject.getPrincipals())
+        {
+            if (principal instanceof AuthenticatedPrincipal)
+            {
+                _principal = principal;
+            }
+        }
+        _port = Mockito.mock(AmqpPort.class);
+        Mockito.doReturn(TEST_PORT).when(_port).getName();
+    }
+
+    @Test
+    public void testFrequencyLimit_multiplePeriods()
+    {
+        final Duration frequencyPeriod1 = Duration.ofSeconds(1L);
+        final Duration frequencyPeriod2 = Duration.ofSeconds(2L);
+
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, Duration.ofMinutes(1L));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_USER, null, 3, frequencyPeriod1));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_USER, null, 2, frequencyPeriod2));
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        final RuleSet ruleSet = builder.build();
+        assertNotNull(ruleSet);
+
+        final Instant registrationStart = Instant.now();
+        try
+        {
+            ruleSet.register(newConnection()).free();
+            ruleSet.register(newConnection()).free();
+        }
+        catch (ConnectionLimitException e)
+        {
+            fail("An exception is not expected here");
+        }
+        final Instant registrationEnd = Instant.now();
+
+        Instant before = Instant.now();
+        do
+        {
+            try
+            {
+                before = Instant.now();
+                ruleSet.register(newConnection()).free();
+                assertTrue(Duration.between(registrationStart, Instant.now()).compareTo(frequencyPeriod2) >= 0);
+                break;
+            }
+            catch (ConnectionLimitException e)
+            {
+                assertTrue(Duration.between(registrationEnd, before).compareTo(frequencyPeriod2) <= 0);
+            }
+        }
+        while (Duration.between(registrationEnd, Instant.now()).compareTo(Duration.ofSeconds(3L)) < 0);
+    }
+
+    @Test
+    public void testGroupConnectionFrequencyLimit_Concurrency()
+    {
+        for (final Integer countLimit : Arrays.asList(57, 176, null))
+        {
+            for (final int threadCount : new int[]{7, 17, 27})
+            {
+                testGroupConnectionFrequencyLimit_Concurrency(countLimit, threadCount);
+            }
+        }
+    }
+
+    private void testGroupConnectionFrequencyLimit_Concurrency(Integer countLimit, int threadCount)
+    {
+        if (countLimit != null)
+        {
+            if (countLimit < 3)
+            {
+                countLimit = 3;
+            }
+            if (countLimit < threadCount)
+            {
+                countLimit = threadCount;
+            }
+        }
+        final Duration frequencyPeriod = Duration.ofDays(3650L);
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, frequencyPeriod);
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_GROUP2, countLimit, 1000, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_GROUP1, null, 2, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1, 1, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP, 1, 1, frequencyPeriod));
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        testParallelThreads(threadCount, builder.build(), true);
+    }
+
+    @Test
+    public void testGroupConnectionCountLimit_Concurrency()
+    {
+        for (final Duration duration : Arrays.asList(Duration.ofDays(2L), null))
+        {
+            for (final Integer frequencyLimit : Arrays.asList(200, 77, null))
+            {
+                for (final int threadCount : new int[]{7, 11, 21})
+                {
+                    testGroupConnectionCountLimit_Concurrency(duration, frequencyLimit, threadCount);
+                }
+            }
+        }
+    }
+
+    private void testGroupConnectionCountLimit_Concurrency(Duration frequencyPeriod, Integer frequencyLimit, int threadCount)
+    {
+        if (frequencyLimit != null)
+        {
+            if (frequencyLimit < 3)
+            {
+                frequencyLimit = 3;
+            }
+            if (frequencyLimit < threadCount)
+            {
+                frequencyLimit = threadCount;
+            }
+        }
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, frequencyPeriod);
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_GROUP2, 1000, frequencyLimit, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_GROUP1, 2, null, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1, 1, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP, 1, 1, frequencyPeriod));
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        testParallelThreads(threadCount, builder.build(), false);
+    }
+
+    @Test
+    public void testUserConnectionFrequencyLimit_Concurrency()
+    {
+        for (final Integer countLimit : Arrays.asList(200, 77, null))
+        {
+            for (int threadCount : new int[]{7, 12, 27})
+            {
+                testUserConnectionFrequencyLimit_Concurrency(countLimit, threadCount);
+            }
+        }
+    }
+
+    private void testUserConnectionFrequencyLimit_Concurrency(Integer countLimit, int threadCount)
+    {
+        if (countLimit != null)
+        {
+            if (countLimit < 3)
+            {
+                countLimit = 3;
+            }
+            if (countLimit < threadCount)
+            {
+                countLimit = threadCount;
+            }
+        }
+        final Duration frequencyPeriod = Duration.ofDays(3650L);
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, frequencyPeriod);
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_USER, countLimit, 2, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1, 1, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP, 1, 1, frequencyPeriod));
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        testParallelThreads(threadCount, builder.build(), true);
+    }
+
+    @Test
+    public void testUserConnectionCountLimit_Concurrency()
+    {
+        for (final Duration duration : Arrays.asList(Duration.ofMinutes(10L), Duration.ofHours(1L), null))
+        {
+            for (final Integer connectionFrequency : Arrays.asList(200, 787, null))
+            {
+                for (final int threadCount : new int[]{5, 10, 20})
+                {
+                    testUserConnectionCountLimit_Concurrency(duration, connectionFrequency, threadCount);
+                }
+            }
+        }
+    }
+
+    private void testUserConnectionCountLimit_Concurrency(Duration duration, Integer connectionFrequency, int threadCount)
+    {
+        if (connectionFrequency != null)
+        {
+            if (connectionFrequency < 3)
+            {
+                connectionFrequency = 3;
+            }
+            if (connectionFrequency < threadCount)
+            {
+                connectionFrequency = threadCount;
+            }
+        }
+
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, duration);
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, TEST_USER, 2, null, duration));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_USER, 2, connectionFrequency, duration));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1, 1, duration));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP, 1, 1, duration));
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        testParallelThreads(threadCount, builder.build(), false);
+    }
+
+    private void testParallelThreads(int threadCount, RuleSet ruleSet, boolean frequencyTest)
+    {
+        assertNotNull(ruleSet);
+
+        final AtomicReference<ConnectionSlot> connection1 = new AtomicReference<>();
+        final AtomicReference<ConnectionLimitException> exception1 = new AtomicReference<>();
+
+        final Thread thread1 = new Thread(() ->
+        {
+            try
+            {
+                connection1.set(ruleSet.register(newConnection()));
+            }
+            catch (ConnectionLimitException e)
+            {
+                exception1.set(e);
+            }
+        });
+
+        final AtomicReference<ConnectionSlot> connection2 = new AtomicReference<>();
+        final AtomicReference<ConnectionLimitException> exception2 = new AtomicReference<>();
+        final Thread thread2 = new Thread(() ->
+        {
+            try
+            {
+                connection2.set(ruleSet.register(newConnection()));
+            }
+            catch (ConnectionLimitException e)
+            {
+                exception2.set(e);
+            }
+        });
+        try
+        {
+            thread1.start();
+            thread2.start();
+            thread1.join(300000L);
+            thread2.join(300000L);
+        }
+        catch (InterruptedException e)
+        {
+            thread1.interrupt();
+            thread2.interrupt();
+            return;
+        }
+        assertNotNull(connection1.get());
+        assertNull(exception1.get());
+        assertNotNull(connection2.get());
+        assertNull(exception2.get());
+
+        int positive = runRegistration(ruleSet, threadCount);
+        if (positive < 0)
+        {
+            return;
+        }
+        assertEquals(0, positive);
+
+        final Thread deThread1 = new Thread(() -> connection1.get().free());
+        final Thread deThread2 = new Thread(() -> connection2.get().free());
+        try
+        {
+            deThread1.start();
+            deThread2.start();
+            deThread1.join(300000L);
+            deThread2.join(300000L);
+        }
+        catch (InterruptedException e)
+        {
+            deThread1.interrupt();
+            deThread2.interrupt();
+            return;
+        }
+
+        positive = runRegistration(ruleSet, threadCount);
+        if (positive < 0)
+        {
+            return;
+        }
+        if (frequencyTest)
+        {
+            assertEquals(0, positive);
+        }
+        else
+        {
+            assertEquals(2, positive);
+        }
+    }
+
+    private int runRegistration(RuleSet ruleSet, int threadCount)
+    {
+        final AtomicInteger positive = new AtomicInteger(threadCount);
+        final Thread[] threads = new Thread[threadCount];
+
+        for (int i = 0; i < threads.length; i++)
+        {
+            threads[i] = new Thread(() ->
+            {
+                try
+                {
+                    ruleSet.register(newConnection());
+                }
+                catch (ConnectionLimitException e)
+                {
+                    positive.decrementAndGet();
+                }
+            });
+        }
+
+        try
+        {
+            Arrays.stream(threads).forEach(Thread::start);
+            for (final Thread thread : threads)
+            {
+                thread.join(300000L);
+            }
+        }
+        catch (InterruptedException e)
+        {
+            Arrays.stream(threads).forEach(Thread::interrupt);
+            return -1;
+        }
+        return positive.get();
+    }
+
+    @Test
+    public void testUserConnectionCountLimit()
+    {
+        for (final Duration duration : Arrays.asList(Duration.ofMinutes(11L), Duration.ofDays(1L), null))
+        {
+            for (final Integer frequencyLimit : Arrays.asList(211, null, 45))
+            {
+                testUserConnectionCountLimit(duration, frequencyLimit);
+            }
+        }
+    }
+
+    private void testUserConnectionCountLimit(Duration duration, Integer frequencyLimit)
+    {
+        if (frequencyLimit != null && frequencyLimit < 3)
+        {
+            frequencyLimit = 3;
+        }
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, duration);
+        builder.addRule(Rule.newNonBlockingRule(
+                RulePredicates.ALL_PORTS, TEST_USER, 2, null, duration));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_USER, 2, frequencyLimit, duration));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1, 1, duration));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP, 1, 1, duration));
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        testConnectionCountLimit2(builder.build());
+        testConnectionCountLimit2(builder.logAllMessages(true).build());
+        testConnectionCountLimit2(builder.logAllMessages(false).build());
+    }
+
+    @Test
+    public void testGroupConnectionCountLimit()
+    {
+        for (final Duration duration : Arrays.asList(Duration.ofMinutes(11L), Duration.ofDays(2L), null))
+        {
+            for (final Integer frequencyLimit : Arrays.asList(217, null, 47))
+            {
+                testGroupConnectionCountLimit(duration, frequencyLimit);
+            }
+        }
+    }
+
+    private void testGroupConnectionCountLimit(Duration duration, Integer frequencyLimit)
+    {
+        if (frequencyLimit != null && frequencyLimit < 3)
+        {
+            frequencyLimit = 3;
+        }
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, duration);
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_GROUP2, 1000, frequencyLimit, duration));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_GROUP1, 2, null, duration));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1, 1, duration));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP, 1, 1, duration));
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        testConnectionCountLimit2(builder.build());
+        testConnectionCountLimit2(builder.logAllMessages(true).build());
+        testConnectionCountLimit2(builder.logAllMessages(false).build());
+    }
+
+    @Test
+    public void testDefaultConnectionCountLimit()
+    {
+        for (final Duration duration : Arrays.asList(Duration.ofMinutes(11L), Duration.ofDays(12L), null))
+        {
+            for (final Integer frequencyLimit : Arrays.asList(117, null, 147))
+            {
+                testDefaultConnectionCountLimit(duration, frequencyLimit);
+            }
+        }
+    }
+
+    private void testDefaultConnectionCountLimit(Duration duration, Integer frequencyLimit)
+    {
+        if (frequencyLimit != null && frequencyLimit < 3)
+        {
+            frequencyLimit = 3;
+        }
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, duration);
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, TEST_USER, 2, frequencyLimit, duration));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1, 1, duration));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP, 1, 1, duration));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 2, null, duration));
+
+        testConnectionCountLimit2(builder.build());
+        testConnectionCountLimit2(builder.logAllMessages(true).build());
+        testConnectionCountLimit2(builder.logAllMessages(false).build());
+    }
+
+    private void testConnectionCountLimit2(RuleSet ruleSet)
+    {
+        assertNotNull(ruleSet);
+
+        ConnectionSlot connection1 = null;
+        ConnectionSlot connection2 = null;
+        try
+        {
+            connection1 = ruleSet.register(newConnection());
+            connection2 = ruleSet.register(newConnection());
+        }
+        catch (ConnectionLimitException e)
+        {
+            fail("No exception is expected: " + e.getMessage());
+        }
+        assertNotNull(connection1);
+        assertNotNull(connection2);
+
+        try
+        {
+            ruleSet.register(newConnection());
+            fail("An exception is expected");
+        }
+        catch (ConnectionLimitException e)
+        {
+            assertEquals("User user breaks connection count limit 2 on port amqp", e.getMessage());
+        }
+
+        connection1.free();
+        ConnectionSlot connection3 = null;
+        try
+        {
+            connection3 = ruleSet.register(newConnection());
+        }
+        catch (ConnectionLimitException e)
+        {
+            fail("No exception is expected: " + e.getMessage());
+        }
+        assertNotNull(connection3);
+
+        connection2.free();
+        connection3.free();
+    }
+
+    @Test
+    public void testBlockedUser()
+    {
+        for (final Duration duration : Arrays.asList(Duration.ofMinutes(11L), Duration.ofDays(1L), null))
+        {
+            testBlockedUser(duration);
+        }
+    }
+
+    private void testBlockedUser(Duration duration)
+    {
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, duration);
+        builder.addRule(Rule.newBlockingRule(TEST_PORT, TEST_USER));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1000, 1000, duration));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP, 1000, 1000, duration));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 1000, 1000, duration));
+
+        testBlocked(builder.build());
+        testBlocked(builder.logAllMessages(true).build());
+        testBlocked(builder.logAllMessages(false).build());
+    }
+
+    @Test
+    public void testBlockedGroup()
+    {
+        for (final Duration duration : Arrays.asList(Duration.ofMinutes(11L), Duration.ofDays(1L), null))
+        {
+            testBlockedGroup(duration);
+        }
+    }
+
+    private void testBlockedGroup(Duration duration)
+    {
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, duration);
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_GROUP1, 10000, null, duration));
+        builder.addRule(Rule.newBlockingRule(TEST_PORT, TEST_GROUP2));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1000, 1000, duration));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP, 1000, 1000, duration));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 10000, 10000, duration));
+
+        testBlocked(builder.build());
+        testBlocked(builder.logAllMessages(true).build());
+        testBlocked(builder.logAllMessages(false).build());
+    }
+
+    @Test
+    public void testBlockedByDefault()
+    {
+        for (final Duration duration : Arrays.asList(Duration.ofMinutes(11L), Duration.ofDays(1L), null))
+        {
+            testBlockedByDefault(duration);
+        }
+    }
+
+    private void testBlockedByDefault(Duration duration)
+    {
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, duration);
+        builder.addRule(Rule.newBlockingRule(TEST_PORT, RulePredicates.ALL_USERS));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 10000, 10000, duration));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1000, 1000, duration));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP, 1000, 1000, duration));
+
+        testBlocked(builder.build());
+        testBlocked(builder.logAllMessages(true).build());
+        testBlocked(builder.logAllMessages(false).build());
+    }
+
+    private void testBlocked(RuleSet ruleSet)
+    {
+        assertNotNull(ruleSet);
+
+        ConnectionSlot connection = null;
+        try
+        {
+            connection = ruleSet.register(newConnection());
+            fail("An exception is expected");
+        }
+        catch (ConnectionLimitException e)
+        {
+            assertEquals("User user is blocked on port amqp", e.getMessage());
+        }
+        assertNull(connection);
+    }
+
+    @Test
+    public void testUserConnectionFrequencyLimit()
+    {
+        for (final Integer countLimit : Arrays.asList(300, 200, null))
+        {
+            testUserConnectionFrequencyLimit(countLimit);
+        }
+    }
+
+    private void testUserConnectionFrequencyLimit(Integer countLimit)
+    {
+        if (countLimit != null && countLimit < 3)
+        {
+            countLimit = 3;
+        }
+        final Duration frequencyPeriod = Duration.ofDays(3650L);
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, frequencyPeriod);
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_USER, countLimit, 2, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1, 1, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP, 1, 1, frequencyPeriod));
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        testConnectionFrequencyLimit2(builder.build());
+        testConnectionFrequencyLimit2(builder.logAllMessages(true).build());
+        testConnectionFrequencyLimit2(builder.logAllMessages(false).build());
+    }
+
+    @Test
+    public void testGroupConnectionFrequencyLimit()
+    {
+        for (final Integer countLimit : Arrays.asList(300, 200, null))
+        {
+            testGroupConnectionFrequencyLimit(countLimit);
+        }
+    }
+
+    private void testGroupConnectionFrequencyLimit(Integer countLimit)
+    {
+        if (countLimit != null && countLimit < 3)
+        {
+            countLimit = 3;
+        }
+        final Duration frequencyPeriod = Duration.ofDays(3650L);
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, frequencyPeriod);
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_GROUP2, countLimit, 1000, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_GROUP1, null, 2, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1, 1, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP, 1, 1, frequencyPeriod));
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        final RuleSet ruleSet = builder.build();
+        testConnectionFrequencyLimit2(ruleSet);
+    }
+
+    @Test
+    public void testDefaultConnectionFrequencyLimit()
+    {
+        for (final Integer countLimit : Arrays.asList(300, 200, null))
+        {
+            testDefaultConnectionFrequencyLimit(countLimit);
+        }
+    }
+
+    private void testDefaultConnectionFrequencyLimit(Integer countLimit)
+    {
+        if (countLimit != null && countLimit < 3)
+        {
+            countLimit = 3;
+        }
+        final Duration frequencyPeriod = Duration.ofDays(3650L);
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, frequencyPeriod);
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, RulePredicates.ALL_USERS, null, 2, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, countLimit, 2, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1, 1, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP, 1, 1, frequencyPeriod));
+
+        final RuleSet ruleSet = builder.build();
+        testConnectionFrequencyLimit2(ruleSet);
+    }
+
+    private void testConnectionFrequencyLimit2(RuleSet ruleSet)
+    {
+        assertNotNull(ruleSet);
+
+        ConnectionSlot connection1 = null;
+        ConnectionSlot connection2 = null;
+
+        try
+        {
+            connection1 = ruleSet.register(newConnection());
+            connection2 = ruleSet.register(newConnection());
+        }
+        catch (ConnectionLimitException e)
+        {
+            fail("An exception is not expected");
+        }
+        assertNotNull(connection1);
+        assertNotNull(connection2);
+
+        try
+        {
+            ruleSet.register(newConnection());
+            fail("An exception is expected here");
+        }
+        catch (ConnectionLimitException e)
+        {
+            assertTrue(Pattern.matches("User user breaks connection frequency limit 2 per \\d+ s on port amqp", e.getMessage()));
+        }
+
+        connection1.free();
+        connection2.free();
+
+        try
+        {
+            ruleSet.register(newConnection());
+            fail("An exception is expected here");
+        }
+        catch (ConnectionLimitException e)
+        {
+            assertTrue(Pattern.matches("User user breaks connection frequency limit 2 per \\d+ s on port amqp", e.getMessage()));
+        }
+    }
+
+    @Test
+    public void testNoLimits()
+    {
+        for (final Duration duration : Arrays.asList(null, Duration.ofNanos(1L), Duration.ofMillis(1L), Duration.ofMinutes(1L), Duration.ofDays(1L)))
+        {
+            testNoLimits(duration);
+        }
+    }
+
+    private void testNoLimits(Duration duration)
+    {
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, duration);
+        builder.addRule(Rule.newBlockingRule(TEST_PORT, OTHER_USER));
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, OTHER_GROUP));
+
+        final RuleSet ruleSet = builder.build();
+        assertNotNull(ruleSet);
+
+        ConnectionSlot connection1 = null;
+        ConnectionSlot connection2 = null;
+        ConnectionSlot connection3 = null;
+
+        try
+        {
+            connection1 = ruleSet.register(newConnection());
+            connection2 = ruleSet.register(newConnection());
+            connection3 = ruleSet.register(newConnection());
+        }
+        catch (ConnectionLimitException e)
+        {
+            fail("An exception is not expected here");
+        }
+        assertNotNull(connection1);
+        assertNotNull(connection2);
+        assertNotNull(connection3);
+
+        connection1.free();
+        connection2.free();
+        connection3.free();
+    }
+
+    @Test
+    public void testRegisterNullUser()
+    {
+        for (final Duration duration : Arrays.asList(null, Duration.ofMinutes(1L), Duration.ofDays(1L)))
+        {
+            testRegisterNullUser(duration);
+        }
+    }
+
+    private void testRegisterNullUser(Duration duration)
+    {
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, duration);
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        final RuleSet ruleSet = builder.build();
+        assertNotNull(ruleSet);
+
+        final AMQPConnection<?> connection = Mockito.mock(AMQPConnection.class);
+        Mockito.doReturn(_port).when(connection).getPort();
+        Mockito.doReturn(_subject).when(connection).getSubject();
+        Mockito.doReturn(_eventLogger).when(connection).getEventLogger();
+
+        try
+        {
+            ruleSet.register(connection);
+            fail("An exception is expected");
+        }
+        catch (ConnectionLimitException e)
+        {
+            assertEquals("Unauthorized connection is forbidden", e.getMessage());
+        }
+    }
+
+    @Test
+    public void testRegisterNullSubject()
+    {
+        for (final Duration duration : Arrays.asList(null, Duration.ofMinutes(1L), Duration.ofDays(1L)))
+        {
+            testRegisterNullSubject(duration);
+        }
+    }
+
+    private void testRegisterNullSubject(Duration duration)
+    {
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, duration);
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, TEST_GROUP1, 1000, 1000, duration));
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, TEST_GROUP2, 1000, 1000, duration));
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        final RuleSet ruleSet = builder.build();
+        assertNotNull(ruleSet);
+
+        final AMQPConnection<?> connection = Mockito.mock(AMQPConnection.class);
+        Mockito.doReturn(_port).when(connection).getPort();
+        Mockito.doReturn(_principal).when(connection).getAuthorizedPrincipal();
+        Mockito.doReturn(_eventLogger).when(connection).getEventLogger();
+
+        try
+        {
+            ruleSet.register(connection);
+            fail("An exception is expected");
+        }
+        catch (ConnectionLimitException e)
+        {
+            assertEquals("User user is blocked on port amqp", e.getMessage());
+        }
+    }
+
+    private AMQPConnection<?> newConnection()
+    {
+        final AMQPConnection<?> connection = Mockito.mock(AMQPConnection.class);
+        Mockito.doReturn(_port).when(connection).getPort();
+        Mockito.doReturn(_subject).when(connection).getSubject();
+        Mockito.doReturn(_principal).when(connection).getAuthorizedPrincipal();
+        Mockito.doReturn(_eventLogger).when(connection).getEventLogger();
+        return connection;
+    }
+
+    @Test
+    public void testBuilder_AddNull()
+    {
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, Duration.ofMillis(2L));
+        builder.addRule(null);
+        builder.addRules(null);
+
+        final RuleSet ruleSet = builder.build();
+
+        assertNotNull(ruleSet);
+
+        final ConnectionSlot connection1 = ruleSet.register(newConnection());
+        assertNotNull(connection1);
+
+        final ConnectionSlot connection2 = ruleSet.register(newConnection());
+        assertNotNull(connection2);
+
+        connection1.free();
+        connection2.free();
+    }
+
+    @Test
+    public void testAppend_CountLimit()
+    {
+        for (final Duration duration : Arrays.asList(Duration.ofMinutes(11L), Duration.ofDays(1L), null))
+        {
+            for (final Integer frequencyLimit : Arrays.asList(211, null, 45))
+            {
+                testAppend_CountLimit(duration, frequencyLimit);
+            }
+        }
+    }
+
+    private void testAppend_CountLimit(Duration duration, Integer frequencyLimit)
+    {
+        if (frequencyLimit != null && frequencyLimit < 3)
+        {
+            frequencyLimit = 3;
+        }
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, duration);
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, TEST_USER, 200, 20000, duration));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1, 1, duration));
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        final Builder secondaryBuilder = RuleSet.newBuilder(LIMITER_NAME, duration);
+        secondaryBuilder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_USER, 2, frequencyLimit, duration));
+        secondaryBuilder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        testConnectionCountLimit2((RuleSet) builder.build().append(secondaryBuilder.build()));
+    }
+
+    @Test
+    public void testAppend_FrequencyLimit()
+    {
+        for (final Integer countLimit : Arrays.asList(300, 200, null))
+        {
+            testAppend_FrequencyLimit(countLimit);
+        }
+    }
+
+    private void testAppend_FrequencyLimit(Integer countLimit)
+    {
+        if (countLimit != null && countLimit < 3)
+        {
+            countLimit = 3;
+        }
+
+        final Duration frequencyPeriod = Duration.ofDays(3650L);
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, frequencyPeriod);
+        builder.addRule(Rule.newNonBlockingRule(RulePredicates.ALL_PORTS, TEST_USER, 200, 20000, frequencyPeriod));
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, OTHER_USER, 1, 1, frequencyPeriod));
+        builder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        final Builder secondaryBuilder = RuleSet.newBuilder(LIMITER_NAME, frequencyPeriod);
+        secondaryBuilder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_USER, countLimit, 2, frequencyPeriod));
+        secondaryBuilder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        testConnectionFrequencyLimit2((RuleSet) builder.build().append(secondaryBuilder.build()));
+    }
+
+    @Test
+    public void testAppend_BlockedUser()
+    {
+        for (final Duration duration : Arrays.asList(Duration.ofMinutes(11L), Duration.ofDays(1L), null))
+        {
+            testAppend_BlockedUser(duration);
+        }
+    }
+
+    private void testAppend_BlockedUser(Duration duration)
+    {
+        final Builder builder = RuleSet.newBuilder(LIMITER_NAME, duration);
+        builder.addRule(Rule.newNonBlockingRule(TEST_PORT, TEST_USER, 1000, 1000, duration));
+
+        final Builder secondaryBuilder = RuleSet.newBuilder(LIMITER_NAME, Duration.ofDays(1L));
+        secondaryBuilder.addRule(Rule.newBlockingRule(RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS));
+
+        testBlocked((RuleSet) builder.build().append(secondaryBuilder.build()));
+    }
+
+    @Test
+    public void testName()
+    {
+        final RuleSet ruleSet = RuleSet.newBuilder(LIMITER_NAME, Duration.ofMinutes(1L)).build();
+        assertEquals(LIMITER_NAME, ruleSet.toString());
+    }
+
+    @Test
+    public void testInvalidRuleWithoutPeriod()
+    {
+        RuleSet.Builder builder = RuleSet.newBuilder(LIMITER_NAME, Duration.ofMinutes(1L));
+        final NonBlockingRule rule = Rule.newNonBlockingRule(
+                RulePredicates.ALL_PORTS, RulePredicates.ALL_USERS, 2, 2, null);
+        try
+        {
+            builder.addRule(rule);
+            fail("An exception is expected, the rule is not valid.");
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+
+        try
+        {
+            builder.addRules(Collections.singletonList(rule));
+            fail("An exception is expected, the rule is not valid.");
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+    }
+}
\ No newline at end of file
diff --git a/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/RuleTest.java b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/RuleTest.java
new file mode 100644
index 0000000..73d7049
--- /dev/null
+++ b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/config/RuleTest.java
@@ -0,0 +1,253 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.config;
+
+import java.time.Duration;
+import java.util.Collections;
+
+import org.junit.Test;
+
+import org.apache.qpid.server.user.connection.limits.config.RulePredicates.Property;
+import org.apache.qpid.server.user.connection.limits.plugins.ConnectionLimitRule;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class RuleTest extends UnitTestBase
+{
+    @Test
+    public void testNewInstanceFromConnectionLimit_NonBlocking()
+    {
+        final ConnectionLimitRule connectionLimit = new ConnectionLimitRule()
+        {
+            @Override
+            public String getPort()
+            {
+                return "Amqp";
+            }
+
+            @Override
+            public String getIdentity()
+            {
+                return "All";
+            }
+
+            @Override
+            public Boolean getBlocked()
+            {
+                return null;
+            }
+
+            @Override
+            public Integer getCountLimit()
+            {
+                return 10;
+            }
+
+            @Override
+            public Integer getFrequencyLimit()
+            {
+                return 30;
+            }
+
+            @Override
+            public Long getFrequencyPeriod()
+            {
+                return 10000L;
+            }
+        };
+        final Rule rule = Rule.newInstance(connectionLimit);
+
+        assertNotNull(rule);
+        assertEquals(connectionLimit.getCountLimit(), rule.getCountLimit());
+        assertEquals(connectionLimit.getFrequencyLimit(), rule.getFrequencyLimit());
+        assertEquals(Duration.ofMillis(connectionLimit.getFrequencyPeriod()), rule.getFrequencyPeriod());
+        assertEquals(
+                Collections.singletonMap(Duration.ofMillis(connectionLimit.getFrequencyPeriod()), rule.getFrequencyLimit()),
+                rule.getFrequencyLimits());
+        assertEquals(connectionLimit.getPort(), rule.getPort());
+        assertEquals(connectionLimit.getIdentity(), rule.getIdentity());
+        assertFalse(rule.isUserBlocked());
+        assertFalse(rule.isEmpty());
+    }
+
+    @Test
+    public void testNewInstanceFromConnectionLimit_Empty()
+    {
+        final ConnectionLimitRule connectionLimit = new ConnectionLimitRule()
+        {
+            @Override
+            public String getPort()
+            {
+                return null;
+            }
+
+            @Override
+            public String getIdentity()
+            {
+                return null;
+            }
+
+            @Override
+            public Boolean getBlocked()
+            {
+                return null;
+            }
+
+            @Override
+            public Integer getCountLimit()
+            {
+                return null;
+            }
+
+            @Override
+            public Integer getFrequencyLimit()
+            {
+                return null;
+            }
+
+            @Override
+            public Long getFrequencyPeriod()
+            {
+                return null;
+            }
+        };
+        try
+        {
+            Rule.newInstance(connectionLimit);
+            fail("An exception is expected, connection limit rule can not be empty");
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+    }
+
+    @Test
+    public void testNewInstanceFromConnectionLimit_Blocking()
+    {
+        final ConnectionLimitRule connectionLimit = new ConnectionLimitRule()
+        {
+            @Override
+            public String getPort()
+            {
+                return "amqp";
+            }
+
+            @Override
+            public String getIdentity()
+            {
+                return "all";
+            }
+
+            @Override
+            public Boolean getBlocked()
+            {
+                return Boolean.TRUE;
+            }
+
+            @Override
+            public Integer getCountLimit()
+            {
+                return null;
+            }
+
+            @Override
+            public Integer getFrequencyLimit()
+            {
+                return null;
+            }
+
+            @Override
+            public Long getFrequencyPeriod()
+            {
+                return null;
+            }
+        };
+        final Rule rule = Rule.newInstance(connectionLimit);
+
+        assertNotNull(rule);
+        assertEquals(Integer.valueOf(0), rule.getCountLimit());
+        assertEquals(Integer.valueOf(0), rule.getFrequencyLimit());
+        assertTrue(rule.getFrequencyLimits().isEmpty());
+        assertEquals(connectionLimit.getPort(), rule.getPort());
+        assertEquals(connectionLimit.getIdentity(), rule.getIdentity());
+        assertTrue(rule.isUserBlocked());
+        assertFalse(rule.isEmpty());
+    }
+
+    @Test
+    public void testNewInstanceFromPredicates_NonBlocking()
+    {
+        final RulePredicates predicates = new RulePredicates();
+        predicates.addProperty(Property.CONNECTION_LIMIT, "10");
+        predicates.addProperty(Property.CONNECTION_FREQUENCY_LIMIT, "20/m");
+        predicates.addProperty(Property.PORT, "Amqps");
+        final Rule rule = Rule.newInstance("User", predicates);
+
+        assertNotNull(rule);
+        assertEquals(predicates.getConnectionCountLimit(), rule.getCountLimit());
+        assertEquals(predicates.getConnectionFrequencyLimit(), rule.getFrequencyLimit());
+        assertEquals(predicates.getConnectionFrequencyPeriod(), rule.getFrequencyPeriod());
+        assertEquals(
+                Collections.singletonMap(predicates.getConnectionFrequencyPeriod(), predicates.getConnectionFrequencyLimit()),
+                rule.getFrequencyLimits());
+        assertEquals(predicates.getPort(), rule.getPort());
+        assertEquals("User", rule.getIdentity());
+        assertFalse(rule.isUserBlocked());
+        assertFalse(rule.isEmpty());
+    }
+
+    @Test
+    public void testNewInstanceFromPredicates_Empty()
+    {
+        final RulePredicates predicates = new RulePredicates();
+        try
+        {
+            Rule.newInstance("user", predicates);
+            fail("An exception is expected, connection limit rule can not be empty");
+        }
+        catch (IllegalArgumentException e)
+        {
+            assertNotNull(e.getMessage());
+        }
+    }
+
+    @Test
+    public void testNewInstanceFromPredicates_Blocking()
+    {
+        final RulePredicates predicates = new RulePredicates();
+        predicates.setBlockedUser();
+        predicates.addProperty(Property.PORT, "amqps");
+        final Rule rule = Rule.newInstance("user", predicates);
+
+        assertNotNull(rule);
+        assertEquals(Integer.valueOf(0), rule.getCountLimit());
+        assertEquals(Integer.valueOf(0), rule.getFrequencyLimit());
+        assertTrue(rule.getFrequencyLimits().isEmpty());
+        assertEquals(predicates.getPort(), rule.getPort());
+        assertEquals("user", rule.getIdentity());
+        assertTrue(rule.isUserBlocked());
+        assertFalse(rule.isEmpty());
+    }
+}
\ No newline at end of file
diff --git a/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/outcome/AcceptRegistrationTest.java b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/outcome/AcceptRegistrationTest.java
new file mode 100644
index 0000000..8254420
--- /dev/null
+++ b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/outcome/AcceptRegistrationTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.outcome;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import org.apache.qpid.server.logging.EventLoggerProvider;
+import org.apache.qpid.server.logging.LogMessage;
+import org.apache.qpid.server.security.limit.ConnectionSlot;
+import org.apache.qpid.server.user.connection.limits.logging.ConnectionLimitEventLogger;
+import org.apache.qpid.server.user.connection.limits.logging.FullConnectionLimitEventLogger;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class AcceptRegistrationTest extends UnitTestBase
+{
+    private org.apache.qpid.server.logging.EventLogger _eventLogger;
+    private FullConnectionLimitEventLogger _logger;
+    private ConnectionLimitEventLogger _rejectLogger;
+    private ConnectionSlot _slot;
+
+    @Before
+    public void setUp()
+    {
+        _eventLogger = Mockito.mock(org.apache.qpid.server.logging.EventLogger.class);
+        final EventLoggerProvider provider = () -> _eventLogger;
+        _logger = new FullConnectionLimitEventLogger("AllLogger", provider);
+        _rejectLogger = new ConnectionLimitEventLogger("RejectLogger", provider);
+        _slot = Mockito.mock(ConnectionSlot.class);
+    }
+
+    @Test
+    public void testLogMessage()
+    {
+        final AcceptRegistration result = AcceptRegistration.newInstance(_slot, "user", 7, "amqp");
+        assertNotNull(result);
+        assertEquals("User user with connection count 7 on amqp port", result.getMessage());
+
+        assertEquals(_slot, result.logMessage(_rejectLogger));
+        Mockito.verify(_eventLogger, Mockito.never()).message(Mockito.any(LogMessage.class));
+
+        assertEquals(_slot, result.logMessage(_logger));
+        final ArgumentCaptor<LogMessage> captor = ArgumentCaptor.forClass(LogMessage.class);
+        Mockito.verify(_eventLogger).message(captor.capture());
+        final LogMessage message = captor.getValue();
+        assertEquals("RL-1001 : Accepted : Opening connection by user : Limiter 'AllLogger': User user with connection count 7 on amqp port",
+                message.toString());
+    }
+
+    @Test
+    public void testFree()
+    {
+        final AcceptRegistration result = AcceptRegistration.newInstance(_slot, "user", 17, "amqps");
+        assertNotNull(result);
+        result.free();
+
+        Mockito.verify(_slot).free();
+    }
+}
\ No newline at end of file
diff --git a/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/outcome/RejectRegistrationTest.java b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/outcome/RejectRegistrationTest.java
new file mode 100644
index 0000000..9ecca5c
--- /dev/null
+++ b/broker-plugins/connection-limits/src/test/java/org/apache/qpid/server/user/connection/limits/outcome/RejectRegistrationTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.qpid.server.user.connection.limits.outcome;
+
+import java.time.Duration;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import org.apache.qpid.server.logging.LogMessage;
+import org.apache.qpid.server.user.connection.limits.logging.ConnectionLimitEventLogger;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class RejectRegistrationTest extends UnitTestBase
+{
+    private org.apache.qpid.server.logging.EventLogger _eventLogger;
+    private ConnectionLimitEventLogger _logger;
+
+    @Before
+    public void setUp()
+    {
+        _eventLogger = Mockito.mock(org.apache.qpid.server.logging.EventLogger.class);
+        _logger = new ConnectionLimitEventLogger("RejectLogger", () -> _eventLogger);
+    }
+
+    @Test
+    public void testFromConnectionCount()
+    {
+        final RejectRegistration resource = RejectRegistration.breakingConnectionCount("user", 10, "amqp");
+        assertNotNull(resource);
+        assertEquals("User user breaks connection count limit 10 on port amqp", resource.logMessage(_logger));
+
+        final ArgumentCaptor<LogMessage> captor = ArgumentCaptor.forClass(LogMessage.class);
+        Mockito.verify(_eventLogger).message(captor.capture());
+        final LogMessage message = captor.getValue();
+        assertEquals("RL-1002 : Rejected : Opening connection by user : Limiter 'RejectLogger': User user breaks connection count limit 10 on port amqp", message.toString());
+    }
+
+    @Test
+    public void testFromConnectionFrequency()
+    {
+        final RejectRegistration resource = RejectRegistration.breakingConnectionFrequency("user", 10, Duration.ofMinutes(1L), "amqp");
+        assertNotNull(resource);
+        assertEquals("User user breaks connection frequency limit 10 per 60 s on port amqp", resource.logMessage(_logger));
+
+        final ArgumentCaptor<LogMessage> captor = ArgumentCaptor.forClass(LogMessage.class);
+        Mockito.verify(_eventLogger).message(captor.capture());
+        final LogMessage message = captor.getValue();
+        assertEquals("RL-1002 : Rejected : Opening connection by user : Limiter 'RejectLogger': User user breaks connection frequency limit 10 per 60 s on port amqp", message.toString());
+    }
+
+    @Test
+    public void testBlockedUser()
+    {
+        final RejectRegistration resource = RejectRegistration.blockedUser("user", "amqp");
+        assertNotNull(resource);
+        assertEquals("User user is blocked on port amqp", resource.logMessage(_logger));
+
+        final ArgumentCaptor<LogMessage> captor = ArgumentCaptor.forClass(LogMessage.class);
+        Mockito.verify(_eventLogger).message(captor.capture());
+        final LogMessage message = captor.getValue();
+        assertEquals("RL-1002 : Rejected : Opening connection by user : Limiter 'RejectLogger': User user is blocked on port amqp", message.toString());
+    }
+}
\ No newline at end of file
diff --git a/broker-plugins/management-http/src/main/java/resources/addConnectionLimitProvider.html b/broker-plugins/management-http/src/main/java/resources/addConnectionLimitProvider.html
new file mode 100644
index 0000000..ad3e04d
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/addConnectionLimitProvider.html
@@ -0,0 +1,64 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+  -->
+<div class="dijitHidden">
+    <div data-dojo-type="dijit/Dialog" data-dojo-props="title:'Connection Limit Provider'"
+         id="addConnectionLimitProvider">
+        <form id="addConnectionLimitProvider.form" method="post" data-dojo-type="dijit/form/Form">
+            <div class="formBox">
+                <div class="clear">
+                    <div class="formLabel-labelCell tableContainer-labelCell">Name*:</div>
+                    <div class="formLabel-controlCell tableContainer-valueCell">
+                        <input type="text" id="addConnectionLimitProvider.name"
+                               data-dojo-type="dijit/form/ValidationTextBox"
+                               data-dojo-props="
+                              name: 'name',
+                              placeHolder: 'connection limit provider name',
+                              required: true,
+                              promptMessage: 'Name of connection limit provider, must be unique',
+                              title: 'Enter a unique connection limit provider name per broker'"/>
+                    </div>
+                </div>
+                <div class="clear">
+                    <div class="formLabel-labelCell tableContainer-labelCell">Type*:</div>
+                    <div class="tableContainer-valueCell formLabel-controlCell">
+                        <select id="addConnectionLimitProvider.type" data-dojo-type="dijit/form/FilteringSelect"
+                                data-dojo-props="
+                                    name: 'type',
+                                    required: true,
+                                    placeHolder: 'select connection limit provider type',
+                                    promptMessage: 'Type of connection limit provider',
+                                    title: 'Select connection limit provider type',
+                                    searchAttr: 'name'">
+                        </select>
+                    </div>
+                </div>
+                <div class="clear">
+                    <div id="addConnectionLimitProvider.typeFields"></div>
+                </div>
+            </div>
+        </form>
+
+        <div class="dijitDialogPaneActionBar qpidDialogPaneActionBar">
+            <button data-dojo-type="dijit/form/Button" id="addConnectionLimitProvider.addButton"
+                    data-dojo-props="label: 'Save'" type="submit"></button>
+            <button data-dojo-type="dijit/form/Button" id="addConnectionLimitProvider.cancelButton"
+                    data-dojo-props="label: 'Cancel'"></button>
+        </div>
+    </div>
+</div>
diff --git a/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/connectionlimitfile/add.html b/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/connectionlimitfile/add.html
new file mode 100644
index 0000000..80f8c57
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/connectionlimitfile/add.html
@@ -0,0 +1,32 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+  -->
+<div>
+    <div id="addConnectionLimitProvider.oldBrowserWarning" class="infoMessage hidden clear"></div>
+    <div class="clear">
+        <div id="addConnectionLimitProvider.serverPathLabel" class="formLabel-labelCell tableContainer-labelCell">Server path or upload*:</div>
+        <input type="text" id="addConnectionLimitProvider.path"
+               data-dojo-type="qpid/common/ResourceWidget"
+               data-dojo-props="
+                              name: 'path',
+                              placeHolder: 'connection limit provider file server path',
+                              required: true,
+                              promptMessage: 'Location of the connection limit provider file on the server',
+                              title: 'Enter the connection limit provider file path'" />
+    </div>
+</div>
diff --git a/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/connectionlimitfile/show.html b/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/connectionlimitfile/show.html
new file mode 100644
index 0000000..94ad3f4
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/connectionlimitfile/show.html
@@ -0,0 +1,42 @@
+<!--
+ -
+ - Licensed to the Apache Software Foundation (ASF) under one
+ - or more contributor license agreements.  See the NOTICE file
+ - distributed with this work for additional information
+ - regarding copyright ownership.  The ASF licenses this file
+ - to you under the Apache License, Version 2.0 (the
+ - "License"); you may not use this file except in compliance
+ - with the License.  You may obtain a copy of the License at
+ -
+ -   http://www.apache.org/licenses/LICENSE-2.0
+ -
+ - Unless required by applicable law or agreed to in writing,
+ - software distributed under the License is distributed on an
+ - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ - KIND, either express or implied.  See the License for the
+ - specific language governing permissions and limitations
+ - under the License.
+ -
+ -->
+<div class="ConnectionLimitFileProvider">
+    <div class="clear">
+        <div class="formLabel-labelCell">Path:</div>
+        <div class="formValue-valueCell path"></div>
+    </div>
+
+    <div class="clear">
+        <div class="formLabel-labelCell">Default frequency period:</div>
+        <div class="formValue-valueCell defaultFrequencyPeriod"></div>
+    </div>
+    <div class="clear">
+        <div data-dojo-type="dijit/TitlePane"
+             data-dojo-props="title: 'Connection Limit Rules'">
+            <div class="rules"></div>
+        </div>
+    </div>
+
+    <div class="alignRight">
+        <button data-dojo-type="dijit/form/Button" type="button" class="resetCounters">Reset connection counters</button>
+        <button data-dojo-type="dijit/form/Button" type="button" class="reload">Reload</button>
+    </div>
+</div>
diff --git a/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/rulebased/add.html b/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/rulebased/add.html
new file mode 100644
index 0000000..24d29cc
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/rulebased/add.html
@@ -0,0 +1,33 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+  -->
+
+<div>
+    <div class="clear">
+        <div class="formLabel-labelCell tableContainer-labelCell">Default frequency period:</div>
+        <input type="text" id="addConnectionLimitProvider.ruleBased.defaultFrequencyPeriod"
+               data-dojo-type="dijit/form/NumberTextBox"
+               data-dojo-props="
+                              name: 'defaultFrequencyPeriod',
+                              required: false,
+                              placeHolder: '60000',
+                              promptMessage: 'Enter the time period of the frequency counter in ms',
+                              title: 'Time period of frequency counter',
+                              invalidMessage:'Please enter an integer'"/>
+    </div>
+</div>
diff --git a/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/rulebased/load.html b/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/rulebased/load.html
new file mode 100644
index 0000000..bc64847
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/rulebased/load.html
@@ -0,0 +1,50 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+  -->
+<div class="dijitHidden">
+    <div data-dojo-type="dijit/Dialog"
+         data-dojo-props="title:'Select connection limit file to load'"
+         id="loadConnectionLimitProvider.ruleBased">
+
+        <div id="loadConnectionLimitProvider.oldBrowserWarning" class="infoMessage hidden clear"></div>
+        <form id="loadConnectionLimitProvider.form"
+              data-dojo-type="dijit/form/Form"
+              method="post"
+              class="loadForm">
+            <div class="clear formBox">
+                <div class="formLabel-labelCell tableContainer-labelCell">Server path or upload*:</div>
+                <input type="text"
+                       id="loadConnectionLimitProvider.path"
+                       class="path"
+                       data-dojo-type="qpid/common/ResourceWidget"
+                       data-dojo-props="
+                              name: 'path',
+                              placeHolder: 'connection limit provider file server path',
+                              required: true,
+                              promptMessage: 'Location of the connection limit provider file on the server',
+                              title: 'Enter the connection limit provider file path'"/>
+            </div>
+        </form>
+        <div class="dijitDialogPaneActionBar qpidDialogPaneActionBar">
+            <button data-dojo-type="dijit/form/Button" id="loadConnectionLimitProvider.submitButton"
+                    data-dojo-props="label: 'Submit'" type="submit" class="submit"></button>
+            <button data-dojo-type="dijit/form/Button" id="loadConnectionLimitProvider.cancelButton"
+                    data-dojo-props="label: 'Cancel'" type="button" class="cancel"></button>
+        </div>
+    </div>
+</div>
diff --git a/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/rulebased/rule.html b/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/rulebased/rule.html
new file mode 100644
index 0000000..753d41c
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/rulebased/rule.html
@@ -0,0 +1,106 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you under the Apache License, Version 2.0 (the
+  ~ "License"); you may not use this file except in compliance
+  ~ with the License.  You may obtain a copy of the License at
+  ~
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing,
+  ~ software distributed under the License is distributed on an
+  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  ~ KIND, either express or implied.  See the License for the
+  ~ specific language governing permissions and limitations
+  ~ under the License.
+  -->
+<div class="dijitHidden">
+    <div data-dojo-type="dijit/Dialog" data-dojo-props="title:'Connection Limit Rule'"
+         id="connectionLimitRule">
+        <form id="connectionLimitRule.form" method="post" data-dojo-type="dijit/form/Form">
+            <div class="formBox">
+                <div class="clear">
+                    <div class="formLabel-labelCell tableContainer-labelCell">Identity*:</div>
+                    <div class="formLabel-controlCell tableContainer-valueCell">
+                        <input type="text" id="connectionLimitRule.name"
+                               data-dojo-type="dijit/form/ValidationTextBox"
+                               data-dojo-props="
+                              name: 'identity',
+                              placeHolder: 'all',
+                              required: true,
+                              promptMessage: 'Enter an identity',
+                              title: 'Identity'"/>
+                    </div>
+                </div>
+                <div class="clear">
+                    <div class="formLabel-labelCell tableContainer-labelCell">Port:</div>
+                    <div class="formLabel-controlCell tableContainer-valueCell">
+                        <input type="text" id="connectionLimitRule.port"
+                               data-dojo-type="dijit/form/ValidationTextBox"
+                               data-dojo-props="
+                              name: 'port',
+                              placeHolder: 'all',
+                              required: false,
+                              promptMessage: 'Enter a port name',
+                              title: 'Port'"/>
+                    </div>
+                </div>
+                <div class="clear">
+                    <div class="formLabel-labelCell tableContainer-labelCell">Blocked user</div>
+                    <div class="formLabel-controlCell tableContainer-valueCell">
+                        <input type="checkbox" id="connectionLimitRule.blocked"
+                               data-dojo-type="dijit/form/CheckBox"
+                               data-dojo-props="
+                                  name: 'blocked',
+                                  required: false,
+                                  checked: false"/>
+                    </div>
+                </div>
+                <div class="clear">
+                    <div class="formLabel-labelCell tableContainer-labelCell">Count limit:</div>
+                    <input type="text" id="connectionLimitRule.countLimit"
+                           data-dojo-type="dijit/form/NumberTextBox"
+                           data-dojo-props="
+                              name: 'countLimit',
+                              required: false,
+                              promptMessage: 'Enter the opening connection count limit',
+                              title: 'Connection count limit',
+                              invalidMessage:'Please enter an integer'"/>
+                </div>
+                <div class="clear">
+                    <div class="formLabel-labelCell tableContainer-labelCell">Frequency limit:</div>
+                    <input type="text" id="connectionLimitRule.frequencyLimit"
+                           data-dojo-type="dijit/form/NumberTextBox"
+                           data-dojo-props="
+                              name: 'frequencyLimit',
+                              required: false,
+                              promptMessage: 'Enter the opening connection frequency limit',
+                              title: 'Connection frequency limit',
+                              invalidMessage:'Please enter an integer'"/>
+                </div>
+                <div class="clear">
+                    <div class="formLabel-labelCell tableContainer-labelCell">Frequency period:</div>
+                    <input type="text" id="connectionLimitRule.frequencyPeriod"
+                           data-dojo-type="dijit/form/NumberTextBox"
+                           data-dojo-props="
+                              name: 'frequencyPeriod',
+                              required: false,
+                              promptMessage: 'Enter the frequency period in ms',
+                              title: 'Frequency period [ms]',
+                              invalidMessage:'Please enter an integer'"/>
+                </div>
+            </div>
+        </form>
+
+        <div class="dijitDialogPaneActionBar qpidDialogPaneActionBar">
+            <button data-dojo-type="dijit/form/Button" id="connectionLimitRule.saveButton"
+                    data-dojo-props="label: 'Save'" type="submit"></button>
+            <button data-dojo-type="dijit/form/Button" id="connectionLimitRule.deleteButton"
+                    data-dojo-props="label: 'Delete'" type="button"></button>
+            <button data-dojo-type="dijit/form/Button" id="connectionLimitRule.cancelButton"
+                    data-dojo-props="label: 'Cancel'" type="button"></button>
+        </div>
+    </div>
+</div>
diff --git a/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/rulebased/show.html b/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/rulebased/show.html
new file mode 100644
index 0000000..cb55826
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/connectionlimitprovider/rulebased/show.html
@@ -0,0 +1,39 @@
+<!--
+ -
+ - Licensed to the Apache Software Foundation (ASF) under one
+ - or more contributor license agreements.  See the NOTICE file
+ - distributed with this work for additional information
+ - regarding copyright ownership.  The ASF licenses this file
+ - to you under the Apache License, Version 2.0 (the
+ - "License"); you may not use this file except in compliance
+ - with the License.  You may obtain a copy of the License at
+ -
+ -   http://www.apache.org/licenses/LICENSE-2.0
+ -
+ - Unless required by applicable law or agreed to in writing,
+ - software distributed under the License is distributed on an
+ - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ - KIND, either express or implied.  See the License for the
+ - specific language governing permissions and limitations
+ - under the License.
+ -
+ -->
+<div class="RuleBasedConnectionLimitProvider">
+    <div class="clear">
+        <div class="formLabel-labelCell">Default frequency period:</div>
+        <div class="formValue-valueCell defaultFrequencyPeriod"></div>
+    </div>
+    <div class="clear">
+        <div data-dojo-type="dijit/TitlePane"
+             data-dojo-props="title: 'Connection Limit Rules'">
+            <div class="rules"></div>
+        </div>
+    </div>
+    <div class="alignRight">
+        <button data-dojo-type="dijit/form/Button" type="button" class="resetCounters">Reset connection counters</button>
+        <button data-dojo-type="dijit/form/Button" type="button" class="addRule">Add</button>
+        <button data-dojo-type="dijit/form/Button" type="button" class="clearRules">Clear</button>
+        <button data-dojo-type="dijit/form/Button" type="button" class="loadRules">Load</button>
+        <button data-dojo-type="dijit/form/Button" type="button" class="extractRules">Extract</button>
+    </div>
+</div>
diff --git a/broker-plugins/management-http/src/main/java/resources/css/common.css b/broker-plugins/management-http/src/main/java/resources/css/common.css
index 8001d37..25cb415 100644
--- a/broker-plugins/management-http/src/main/java/resources/css/common.css
+++ b/broker-plugins/management-http/src/main/java/resources/css/common.css
@@ -852,6 +852,11 @@
 .queueMessages .field-state { width: 20%; }
 .queueMessages .field-arrivalTime { width: auto }
 
+.connectionLimitProviders .field-selected { width: 2em; }
+.connectionLimitProviders .field-name { width: 40%; }
+.connectionLimitProviders .field-state { width: 25%; }
+.connectionLimitProviders .field-type { width: auto;}
+
 .mapList-scroll-y
 {
     height: 5em;
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Broker.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Broker.js
index 98c29fd..4f8fb94 100644
--- a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Broker.js
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Broker.js
@@ -45,6 +45,7 @@
         "qpid/management/addAccessControlProvider",
         "qpid/management/editBroker",
         "qpid/management/addLogger",
+        "qpid/management/addConnectionLimitProvider",
         "dojo/text!showBroker.html",
         "dojox/grid/enhanced/plugins/Pagination",
         "dojox/grid/enhanced/plugins/IndirectSelection",
@@ -87,6 +88,7 @@
               addAccessControlProvider,
               editBroker,
               addLogger,
+              addConnectionLimitProvider,
               template)
     {
 
@@ -328,6 +330,26 @@
                                 that.brokerUpdater);
                         });
 
+                        const addConnectionLimitProviderButton = registry.byNode(
+                            query(".addConnectionLimitProvider", contentPane.containerNode)[0]);
+                        connect.connect(addConnectionLimitProviderButton, "onClick", function (evt)
+                        {
+                            addConnectionLimitProvider.show(that.management, that.modelObj);
+                        });
+
+                        const deleteConnectionLimitProviderButton = registry.byNode(
+                            query(".deleteConnectionLimitProvider", contentPane.containerNode)[0]);
+                        connect.connect(deleteConnectionLimitProviderButton, "onClick", function (evt)
+                        {
+                            util.deleteSelectedObjects(that.brokerUpdater.connectionLimitProvidersGrid.grid,
+                                "Are you sure you want to delete connection limit provider",
+                                that.management,
+                                {
+                                    type: "brokerconnectionlimitprovider",
+                                    parent: that.modelObj
+                                },
+                                that.brokerUpdater);
+                        });
                     });
             }
         };
@@ -896,6 +918,29 @@
                     that.controller.showById(theItem.id);
                 });
             }, gridProperties, EnhancedGrid);
+
+            this.connectionLimitProvidersGrid =
+                new UpdatableStore([], query(".broker-connection-limit-providers")[0], [{
+                    name: "Name",
+                    field: "name",
+                    width: "40%"
+                }, {
+                    name: "State",
+                    field: "state",
+                    width: "30%"
+                }, {
+                    name: "Type",
+                    field: "type",
+                    width: "30%"
+                }], function (obj)
+                {
+                    connect.connect(obj.grid, "onRowDblClick", obj.grid, function (evt)
+                    {
+                        const theItem = this.getItem(evt.rowIndex);
+                        that.controller.showById(theItem.id);
+                    });
+                }, gridProperties, EnhancedGrid);
+
             this.update(function ()
             {
                 updater.add(that);
@@ -1044,6 +1089,11 @@
                     {
                         that.brokerLoggersGrid.update(that.brokerData.brokerloggers);
                     }
+                    if (that.connectionLimitProvidersGrid)
+                    {
+                        const limiters = that.brokerData.brokerconnectionlimitproviders;
+                        that.connectionLimitProvidersGrid.update(limiters);
+                    }
                     if (callback)
                     {
                         callback();
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/ConnectionLimitProvider.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/ConnectionLimitProvider.js
new file mode 100644
index 0000000..84b0e7e
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/ConnectionLimitProvider.js
@@ -0,0 +1,169 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+define(["dojo/parser",
+        "dojo/query",
+        "dojo/_base/connect",
+        "qpid/common/updater",
+        "qpid/common/util",
+        "dijit/registry",
+        "dojo/_base/event",
+        "dojox/html/entities",
+        "dojo/text!showConnectionLimitProvider.html",
+        "dijit/TitlePane",
+        "dijit/form/Button",
+        "dojo/domReady!"],
+    function (parser,
+              query,
+              connect,
+              updater,
+              util,
+              registry,
+              event,
+              entities,
+              template)
+    {
+        function ConnectionLimitProvider(kwArgs)
+        {
+            this.controller = kwArgs.controller;
+            this.modelObj = kwArgs.tabData.modelObject;
+            this.management = kwArgs.controller.management;
+            this.name = kwArgs.tabData.modelObject.name;
+        }
+
+        ConnectionLimitProvider.prototype.getTitle = function ()
+        {
+            return "ConnectionLimitProvider: " + this.name;
+        };
+
+        ConnectionLimitProvider.prototype.open = function (contentPane)
+        {
+            const that = this;
+            this.contentPane = contentPane;
+
+            contentPane.containerNode.innerHTML = template;
+            parser.parse(contentPane.containerNode)
+                .then(function ()
+                {
+                    const deleteButton = query(".deleteConnectionLimitProviderButton", contentPane.containerNode)[0];
+                    const deleteWidget = registry.byNode(deleteButton);
+                    connect.connect(deleteWidget, "onClick", function (evt)
+                    {
+                        event.stop(evt);
+                        that.deleteConnectionLimitProvider();
+                    });
+
+                    that._ConnectionLimitProviderUpdater = new ConnectionLimitProviderUpdater(that);
+                    that._ConnectionLimitProviderUpdater.update(function ()
+                    {
+                        updater.add(that._ConnectionLimitProviderUpdater);
+                    });
+                });
+        };
+
+        ConnectionLimitProvider.prototype.close = function ()
+        {
+            updater.remove(this._ConnectionLimitProviderUpdater);
+        };
+
+        ConnectionLimitProvider.prototype.deleteConnectionLimitProvider = function ()
+        {
+            if (confirm("Are you sure you want to delete user connection limit provider '" + this.name + "'?"))
+            {
+                const that = this;
+                this.controller.management.remove(this.modelObj)
+                    .then(function ()
+                    {
+                        that.close();
+                        that.contentPane.onClose()
+                        that.controller.tabContainer.removeChild(that.contentPane);
+                        that.contentPane.destroyRecursive();
+                    }, util.xhrErrorHandler);
+            }
+        };
+
+        function ConnectionLimitProviderUpdater(tab)
+        {
+            this.contentPane = tab.contentPane;
+            this.controller = tab.controller;
+            this.management = this.controller.management;
+            this.modelObj = tab.modelObj;
+
+            const node = tab.contentPane.containerNode;
+            this._name = query(".name", node)[0];
+            this._type = query(".type", node)[0];
+            this._state = query(".state", node)[0];
+            this._providerDetails = query(".providerDetails", node)[0];
+        }
+
+        ConnectionLimitProviderUpdater.prototype.updateHeader = function ()
+        {
+            this._name.innerHTML = entities.encode(String(this._connectionLimitProviderData["name"]));
+            this._type.innerHTML = entities.encode(String(this._connectionLimitProviderData["type"]));
+            this._state.innerHTML = entities.encode(String(this._connectionLimitProviderData["state"]));
+        };
+
+        ConnectionLimitProviderUpdater.prototype.update = function (callback) {
+            if (!this.contentPane.selected && !callback)
+            {
+                return;
+            }
+
+            const that = this;
+            this.management
+                .load(this.modelObj)
+                .then(function (data) {
+                        that._update(data, callback);
+                    },
+                    function (error) {
+                        util.tabErrorHandler(error, {
+                            updater: that,
+                            contentPane: that.contentPane,
+                            tabContainer: that.controller.tabContainer,
+                            name: that.modelObj.name,
+                            category: "User Connection Limit Provider"
+                        });
+                    });
+        };
+
+        ConnectionLimitProviderUpdater.prototype._update = function (data, callback) {
+            this._connectionLimitProviderData = data;
+            this.updateHeader();
+            if (this._specificProvider)
+            {
+                this._specificProvider.update(data);
+            }
+            else
+            {
+                const that = this;
+                require(["qpid/management/connectionlimitprovider/" + data.type.toLowerCase() + "/show"],
+                    function (SpecificProvider) {
+                        that._specificProvider = new SpecificProvider(that._providerDetails, that.modelObj, that.controller);
+                        that._specificProvider.update(data);
+                        if (callback)
+                        {
+                            callback();
+                        }
+                    });
+            }
+        };
+
+        return ConnectionLimitProvider;
+    });
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/VirtualHost.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/VirtualHost.js
index 344d455..c19a9f8 100644
--- a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/VirtualHost.js
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/VirtualHost.js
@@ -110,8 +110,8 @@
                     registry.byNode(deleteQueueButton).on("click", function (evt)
                     {
                         that._deleteSelectedItems(that.vhostUpdater.queuesGrid,
-                                                  {type: "queue", parent: that.modelObj},
-                                                  "delete", "queue");
+                            {type: "queue", parent: that.modelObj},
+                            "delete", "queue");
                     });
 
                     var addExchangeButton = query(".addExchangeButton", containerNode)[0];
@@ -124,16 +124,16 @@
                     registry.byNode(deleteExchangeButton).on("click", function (evt)
                     {
                         that._deleteSelectedItems(that.vhostUpdater.exchangesGrid,
-                                                 {type: "exchange", parent: that.modelObj},
-                                                 "delete", "exchange");
+                            {type: "exchange", parent: that.modelObj},
+                            "delete", "exchange");
                     });
 
                     var closeConnectionButton = query(".closeConnectionButton", containerNode)[0];
                     registry.byNode(closeConnectionButton).on("click", function (evt)
                     {
                         that._deleteSelectedItems(that.vhostUpdater.connectionsGrid,
-                                                  {type: "connection"},
-                                                  "close", "connection");
+                            {type: "connection"},
+                            "close", "connection");
                     });
 
                     var addLoggerButtonNode = query(".addVirtualHostLogger", contentPane.containerNode)[0];
@@ -148,8 +148,8 @@
                     deleteLoggerButton.on("click", function (evt)
                     {
                         that._deleteSelectedItems(that.vhostUpdater.virtualHostLoggersGrid,
-                                                  {type: "virtualhostlogger", parent: that.modelObj},
-                                                  "delete", "virtual host logger");
+                            {type: "virtualhostlogger", parent: that.modelObj},
+                            "delete", "virtual host logger");
                     });
 
                     that.stopButton = registry.byNode(query(".stopButton", containerNode)[0]);
@@ -168,8 +168,8 @@
                     that.deleteButton.on("click", function (e)
                     {
                         if (confirm("Deletion of virtual host will delete message data.\n\n"
-                                    + "Are you sure you want to delete virtual host  '"
-                                    + entities.encode(String(that.name)) + "'?"))
+                            + "Are you sure you want to delete virtual host  '"
+                            + entities.encode(String(that.name)) + "'?"))
                         {
                             that.management.remove(that.modelObj)
                                 .then(function (result)
@@ -188,8 +188,8 @@
                     that.stopButton.on("click", function (event)
                     {
                         if (confirm("Stopping the virtual host will also stop its children. "
-                                    + "Are you sure you want to stop virtual host '"
-                                    + entities.encode(String(that.name)) + "'?"))
+                            + "Are you sure you want to stop virtual host '"
+                            + entities.encode(String(that.name)) + "'?"))
                         {
                             that.stopButton.set("disabled", true);
                             that.management.update(that.modelObj, {desiredState: "STOPPED"})
@@ -224,6 +224,27 @@
                             "delete", "virtual host access control provider");
                     });
 
+                    const addConnectionLimitProviderButton = registry.byNode(
+                        query(".addVirtualHostConnectionLimitProvider", contentPane.containerNode)[0]);
+                    addConnectionLimitProviderButton.on("click", function () {
+                        require(["qpid/management/addConnectionLimitProvider"],
+                            function (addConnectionLimitProvider)
+                            {
+                                addConnectionLimitProvider.show(that.management, that.modelObj);
+                            });
+                    });
+
+                    const deleteConnectionLimitProviderButton = registry.byNode(
+                        query(".deleteVirtualHostConnectionLimitProvider", contentPane.containerNode)[0]);
+                    deleteConnectionLimitProviderButton.on("click", function () {
+                        that._deleteSelectedItems(that.vhostUpdater.virtualHostConnectionLimitProviderGrid,
+                            {
+                                type: "virtualhostconnectionlimitprovider",
+                                parent: that.modelObj
+                            },
+                            "delete", "virtual host connection limit provider");
+                    });
+
                     that.vhostUpdater.update(function ()
                     {
                         updater.add(that.vhostUpdater);
@@ -233,9 +254,10 @@
 
         VirtualHost.prototype._deleteSelectedItems = function (dgrid, modelObj, friendlyAction, friendlyCategoryName)
         {
+            let confirmed = false;
             var selected = [];
             var selection = dgrid.selection;
-            for(var item in selection)
+            for (var item in selection)
             {
                 if (selection.hasOwnProperty(item) && selection[item])
                 {
@@ -246,10 +268,10 @@
             {
                 var plural = selected.length === 1 ? "" : "s";
                 if (confirm(lang.replace("Are you sure you want to {0} {1} {2}{3}?",
-                        [friendlyAction,
-                         selected.length,
-                         entities.encode(friendlyCategoryName),
-                         plural])))
+                    [friendlyAction,
+                        selected.length,
+                        entities.encode(friendlyCategoryName),
+                        plural])))
                 {
                     this.management
                         .remove(modelObj, {"id": selected})
@@ -258,8 +280,10 @@
                             dgrid.clearSelection();
                             this.vhostUpdater.update();
                         }));
+                    confirmed = true;
                 }
             }
+            return confirmed;
         };
 
         VirtualHost.prototype.close = function ()
@@ -304,22 +328,21 @@
             this.virtualhostStatisticsNode = findNode("virtualhostStatistics");
 
             storeNodes(["name",
-                        "type",
-                        "state",
-                        "durable",
-                        "lifetimePolicy",
-                        "virtualHostDetailsContainer",
-                        "connectionThreadPoolSize",
-                        "statisticsReportingPeriod",
-                        "housekeepingCheckPeriod",
-                        "housekeepingThreadCount",
-                        "storeTransactionIdleTimeoutClose",
-                        "storeTransactionIdleTimeoutWarn",
-                        "storeTransactionOpenTimeoutClose",
-                        "storeTransactionOpenTimeoutWarn",
-                        "virtualHostConnections",
-                        "virtualHostChildren"]);
-
+                "type",
+                "state",
+                "durable",
+                "lifetimePolicy",
+                "virtualHostDetailsContainer",
+                "connectionThreadPoolSize",
+                "statisticsReportingPeriod",
+                "housekeepingCheckPeriod",
+                "housekeepingThreadCount",
+                "storeTransactionIdleTimeoutClose",
+                "storeTransactionIdleTimeoutWarn",
+                "storeTransactionOpenTimeoutClose",
+                "storeTransactionOpenTimeoutWarn",
+                "virtualHostConnections",
+                "virtualHostChildren"]);
 
 
             var CustomGrid = declare([QueryGrid, Selector]);
@@ -364,7 +387,9 @@
                     }
                 ]
             }, findNode("queues"));
-            this.queuesGrid.on('rowBrowsed', function(event){controller.showById(event.id);});
+            this.queuesGrid.on('rowBrowsed', function (event) {
+                controller.showById(event.id);
+            });
             this.queuesGrid.startup();
 
             this.exchangesGrid = new CustomGrid({
@@ -390,7 +415,7 @@
                         field: "selected",
                         label: 'All',
                         selector: 'checkbox'
-                    },  {
+                    }, {
                         label: "Name",
                         field: "name"
                     }, {
@@ -402,7 +427,9 @@
                     }
                 ]
             }, findNode("exchanges"));
-            this.exchangesGrid.on('rowBrowsed', function(event){controller.showById(event.id);});
+            this.exchangesGrid.on('rowBrowsed', function (event) {
+                controller.showById(event.id);
+            });
             this.exchangesGrid.startup();
 
             this.connectionsGrid = new CustomGrid({
@@ -418,41 +445,43 @@
                 deselectOnRefresh: false,
                 allowSelectAll: true,
                 columns: [
-                {
-                    field: "selected",
-                    label: 'All',
-                    selector: 'checkbox'
-                }, {
-                    label: "Name",
-                    field: "name"
-                }, {
-                    label: "User",
-                    field: "principal"
-                }, {
-                    label: "Port",
-                    field: "port"
-                }, {
-                    label: "Transport",
-                    field: "transport"
-                }, {
-                    label: "Sessions",
-                    field: "sessionCount"
-                }, {
-                    label: "Msgs In",
-                    field: "msgInRate"
-                }, {
-                    label: "Bytes In",
-                    field: "bytesInRate"
-                }, {
-                    label: "Msgs Out",
-                    field: "msgOutRate"
-                }, {
-                    label: "Bytes Out",
-                    field: "bytesOutRate"
-                }
+                    {
+                        field: "selected",
+                        label: 'All',
+                        selector: 'checkbox'
+                    }, {
+                        label: "Name",
+                        field: "name"
+                    }, {
+                        label: "User",
+                        field: "principal"
+                    }, {
+                        label: "Port",
+                        field: "port"
+                    }, {
+                        label: "Transport",
+                        field: "transport"
+                    }, {
+                        label: "Sessions",
+                        field: "sessionCount"
+                    }, {
+                        label: "Msgs In",
+                        field: "msgInRate"
+                    }, {
+                        label: "Bytes In",
+                        field: "bytesInRate"
+                    }, {
+                        label: "Msgs Out",
+                        field: "msgOutRate"
+                    }, {
+                        label: "Bytes Out",
+                        field: "bytesOutRate"
+                    }
                 ]
             }, findNode("connections"));
-            this.connectionsGrid.on('rowBrowsed', function(event){controller.showById(event.id);});
+            this.connectionsGrid.on('rowBrowsed', function (event) {
+                controller.showById(event.id);
+            });
             this.connectionsGrid.startup();
 
             this.virtualHostLoggersGrid = new CustomGrid({
@@ -472,7 +501,7 @@
                         field: "selected",
                         label: 'All',
                         selector: 'checkbox'
-                    },  {
+                    }, {
                         label: "Name",
                         field: "name"
                     }, {
@@ -487,7 +516,9 @@
                     }
                 ]
             }, findNode("loggers"));
-            this.virtualHostLoggersGrid.on('rowBrowsed', function(event){controller.showById(event.id);});
+            this.virtualHostLoggersGrid.on('rowBrowsed', function (event) {
+                controller.showById(event.id);
+            });
             this.virtualHostLoggersGrid.startup();
 
             var Store = MemoryStore.createSubclass(TrackableStore);
@@ -507,7 +538,8 @@
                 pageSizeOptions: [10, 20, 30, 40, 50, 100],
                 adjustLastColumn: true,
                 collection: this._policyStore,
-                highlightRow: function (){},
+                highlightRow: function () {
+                },
                 columns: [
                     {
                         label: 'Node Type',
@@ -527,16 +559,16 @@
                         label: "Attributes",
                         field: "attributes",
                         sortable: false,
-                        formatter: function(value, object)
+                        formatter: function (value, object)
                         {
                             var markup = "";
                             if (value)
                             {
                                 markup = "<div class='keyValuePair'>";
-                                for(var key in value)
+                                for (var key in value)
                                 {
                                     markup += "<div>" + entities.encode(String(key)) + "="
-                                              + entities.encode(String(value[key])) + "</div>";
+                                        + entities.encode(String(value[key])) + "</div>";
                                 }
                                 markup += "</div>"
                             }
@@ -549,7 +581,7 @@
             this._policyGrid.startup();
             this._nodeAutoCreationPolicies = registry.byNode(findNode("nodeAutoCreationPolicies"));
 
-            aspect.after(this._nodeAutoCreationPolicies, "toggle", lang.hitch(this, function() {
+            aspect.after(this._nodeAutoCreationPolicies, "toggle", lang.hitch(this, function () {
                 if (this._nodeAutoCreationPolicies.get("open") === true)
                 {
                     this._policyGrid.refresh();
@@ -573,7 +605,7 @@
                         field: "selected",
                         label: 'All',
                         selector: 'checkbox'
-                    },  {
+                    }, {
                         label: "Name",
                         field: "name"
                     }, {
@@ -588,9 +620,44 @@
                     }
                 ]
             }, findNode("virtualHostAccessControlProviders"));
-            this.virtualHostAccessControlProviderGrid.on('rowBrowsed', function(event){controller.showById(event.id);});
+            this.virtualHostAccessControlProviderGrid.on('rowBrowsed', function (event) {
+                controller.showById(event.id);
+            });
             this.virtualHostAccessControlProviderGrid.startup();
 
+            this.virtualHostConnectionLimitProviderGrid = new CustomGrid({
+                detectChanges: true,
+                rowsPerPage: 10,
+                transformer: util.queryResultToObjects,
+                management: this.management,
+                parentObject: this.modelObj,
+                category: "VirtualHostConnectionLimitProvider",
+                selectClause: "id, name, state, type",
+                orderBy: "name",
+                selectionMode: 'none',
+                deselectOnRefresh: false,
+                allowSelectAll: true,
+                columns: [
+                    {
+                        field: "selected",
+                        label: 'All',
+                        selector: 'checkbox'
+                    }, {
+                        label: "Name",
+                        field: "name"
+                    }, {
+                        label: "State",
+                        field: "state"
+                    }, {
+                        label: "Type",
+                        field: "type"
+                    }
+                ]
+            }, findNode("virtualHostConnectionLimitProviders"));
+            this.virtualHostConnectionLimitProviderGrid.on('rowBrowsed', function (event) {
+                controller.showById(event.id);
+            });
+            this.virtualHostConnectionLimitProviderGrid.startup();
         }
 
         Updater.prototype.update = function (callback)
@@ -620,7 +687,7 @@
                 }));
         };
 
-        Updater.prototype._updateFromData = function(data, callback)
+        Updater.prototype._updateFromData = function (data, callback)
         {
             this.vhostData = data || {name: this.modelObj.name};
 
@@ -675,6 +742,7 @@
                 this.exchangesGrid.updateData();
                 this.virtualHostLoggersGrid.updateData();
                 this.virtualHostAccessControlProviderGrid.updateData();
+                this.virtualHostConnectionLimitProviderGrid.updateData();
             }
 
             if (this.details)
@@ -706,13 +774,13 @@
             this.lifetimePolicy.innerHTML = entities.encode(String(this.vhostData["lifetimePolicy"]));
             util.updateUI(this.vhostData,
                 ["housekeepingCheckPeriod",
-                 "housekeepingThreadCount",
-                 "storeTransactionIdleTimeoutClose",
-                 "storeTransactionIdleTimeoutWarn",
-                 "storeTransactionOpenTimeoutClose",
-                 "storeTransactionOpenTimeoutWarn",
-                 "statisticsReportingPeriod",
-                 "connectionThreadPoolSize"],
+                    "housekeepingThreadCount",
+                    "storeTransactionIdleTimeoutClose",
+                    "storeTransactionIdleTimeoutWarn",
+                    "storeTransactionOpenTimeoutClose",
+                    "storeTransactionOpenTimeoutWarn",
+                    "statisticsReportingPeriod",
+                    "connectionThreadPoolSize"],
                 this)
         };
 
@@ -744,7 +812,7 @@
                     if (oldConnection)
                     {
                         msgOutRate = (1000 * (connection.messagesOut - oldConnection.messagesOut))
-                                         / samplePeriod;
+                            / samplePeriod;
                         bytesOutRate = (1000 * (connection.bytesOut - oldConnection.bytesOut)) / samplePeriod;
                         msgInRate = (1000 * (connection.messagesIn - oldConnection.messagesIn)) / samplePeriod;
                         bytesInRate = (1000 * (connection.bytesIn - oldConnection.bytesIn)) / samplePeriod;
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/addConnectionLimitProvider.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/addConnectionLimitProvider.js
new file mode 100644
index 0000000..d54623d
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/addConnectionLimitProvider.js
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+define([
+    "dojo/dom",
+    "dojo/dom-construct",
+    "dijit/registry",
+    "dojo/parser",
+    "dojo/_base/array",
+    "dojo/_base/event",
+    "qpid/common/util",
+    "dojo/text!addConnectionLimitProvider.html",
+    "dijit/Dialog",
+    "dijit/form/Form",
+    "dijit/form/TextBox",
+    "dijit/form/ValidationTextBox",
+    "dijit/form/FilteringSelect",
+    "dijit/form/Button",
+    "dojo/domReady!"], function (dom, construct, registry, parser, array, event, util, template)
+{
+    const addConnectionLimitProvider = {
+        init: function ()
+        {
+            const that = this;
+            this.containerNode = construct.create("div", {innerHTML: template});
+            parser.parse(this.containerNode)
+                .then(function ()
+                {
+                    that._dialog = registry.byId("addConnectionLimitProvider");
+
+                    const providerName = registry.byId("addConnectionLimitProvider.name");
+                    providerName.set("regExpGen", util.nameOrContextVarRegexp);
+
+                    const addButton = registry.byId("addConnectionLimitProvider.addButton");
+                    addButton.on("click", function (e)
+                    {
+                        that._add(e);
+                    });
+
+                    const cancelButton = registry.byId("addConnectionLimitProvider.cancelButton");
+                    cancelButton.on("click", function (e)
+                    {
+                        that._cancel(e);
+                    });
+
+                    that._providerTypeFieldsContainer = dom.byId("addConnectionLimitProvider.typeFields");
+                    that._providerForm = registry.byId("addConnectionLimitProvider.form");
+                    that._providerType = registry.byId("addConnectionLimitProvider.type");
+                    that._providerType.on("change", function (type)
+                    {
+                        that._providerTypeChanged(type);
+                    });
+                });
+            return this;
+        },
+
+        show: function (management, modelObj)
+        {
+            this.management = management;
+            this.modelObj = modelObj;
+            this._providerForm.reset();
+
+            this._category =
+                modelObj && (modelObj.type === "virtualhost" || modelObj.type === "virtualhostconnectionlimitprovider")
+                    ? "VirtualHostConnectionLimitProvider"
+                    : "BrokerConnectionLimitProvider";
+            const supportedProviderTypes = management.metadata.getTypesForCategory(this._category);
+            supportedProviderTypes.sort();
+
+            this._providerType.set("store", util.makeTypeStore(supportedProviderTypes));
+            this._dialog.show();
+        },
+
+        _cancel: function (e)
+        {
+            event.stop(e);
+            this._destroyTypeFields();
+            this._dialog.hide();
+        },
+
+        _add: function (e)
+        {
+            event.stop(e);
+            this._submit();
+        },
+
+        _submit: function ()
+        {
+            if (this._providerForm.validate())
+            {
+                const data = util.getFormWidgetValues(this._providerForm, this.initialData);
+                const that = this;
+                this.management.create(this._category, this.modelObj, data)
+                    .then(function ()
+                    {
+                        that._dialog.hide();
+                    });
+            }
+            else
+            {
+                alert('Form contains invalid data. Please correct first');
+            }
+        },
+
+        _providerTypeChanged: function (type)
+        {
+            this._destroyTypeFields();
+            if (type)
+            {
+                const that = this;
+                require(["qpid/management/connectionlimitprovider/" + type.toLowerCase() + "/add"], function (typeUI)
+                {
+                    try
+                    {
+                        typeUI.show({
+                            containerNode: that._providerTypeFieldsContainer,
+                            parent: that,
+                            initialData: that.initialData || {},
+                            metadata: that.management.metadata,
+                            effectiveData: {},
+                            category: that._category,
+                            type: type
+                        });
+                    }
+                    catch (e)
+                    {
+                        console.warn(e);
+                    }
+                });
+            }
+        },
+
+        _destroyTypeFields: function ()
+        {
+            const widgets = registry.findWidgets(this._providerTypeFieldsContainer);
+            array.forEach(widgets, function (item)
+            {
+                item.destroyRecursive();
+            });
+            construct.empty(this._providerTypeFieldsContainer);
+        }
+    };
+
+    try
+    {
+        addConnectionLimitProvider.init();
+    }
+    catch (e)
+    {
+        console.warn(e);
+    }
+    return addConnectionLimitProvider;
+});
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/connectionlimitfile/add.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/connectionlimitfile/add.js
new file mode 100644
index 0000000..fccef6c
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/connectionlimitfile/add.js
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+define(["dojo/dom",
+        "qpid/common/util",
+        "qpid/common/ResourceWidget"],
+    function (dom, util)
+    {
+        return {
+            show: function (data)
+            {
+                util.parseHtmlIntoDiv(data.containerNode,
+                    "connectionlimitprovider/connectionlimitfile/add.html",
+                    function ()
+                    {
+                        if (!window.FileReader)
+                        {
+                            const odBrowserWarning = dom.byId("addConnectionLimitProvider.oldBrowserWarning");
+                            odBrowserWarning.innerHTML = "File upload requires a more recent browser with HTML5 support";
+                            odBrowserWarning.className = odBrowserWarning.className.replace("hidden", "");
+                        }
+
+                        util.applyToWidgets(data.containerNode,
+                            data.category,
+                            data.type,
+                            data.initialData,
+                            data.metadata,
+                            data.effectiveData);
+                    });
+            }
+        };
+    });
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/connectionlimitfile/show.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/connectionlimitfile/show.js
new file mode 100644
index 0000000..35160ef
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/connectionlimitfile/show.js
@@ -0,0 +1,193 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+define(["dojo/_base/declare",
+        "dojo/_base/event",
+        "dojo/parser",
+        "dojo/query",
+        "dojo/dom-construct",
+        "dijit/registry",
+        "dojox/html/entities",
+        "dgrid/Grid",
+        "dgrid/extensions/Pagination",
+        "dgrid/extensions/ColumnResizer",
+        "dstore/Memory",
+        "dojo/text!connectionlimitprovider/connectionlimitfile/show.html",
+        "dijit/form/Button",
+        "dojo/domReady!"],
+    function (declare,
+              event,
+              parser,
+              query,
+              construct,
+              registry,
+              entities,
+              Grid,
+              Pagination,
+              ColumnResizer,
+              MemoryStore,
+              template)
+    {
+        function ConnectionLimitFile(containerNode, modelObj, controller)
+        {
+            const that = this;
+            this.modelObj = modelObj;
+            this.management = controller.management;
+
+            const node = construct.create("div", {innerHTML: template}, containerNode, "last");
+
+            this._dataSore = new MemoryStore({data: []});
+
+            parser.parse(containerNode)
+                .then(function ()
+                {
+                    that._path = query(".path", node)[0];
+                    that._frequencyPeriod = query(".defaultFrequencyPeriod", node)[0];
+
+                    that._reloadButton = registry.byNode(query(".reload", node)[0]);
+                    that._reloadButton.on("click", function (e)
+                    {
+                        that._reload(e)
+                    });
+
+                    that._resetButton = registry.byNode(query(".resetCounters", node)[0]);
+                    that._resetButton.on("click", function (e)
+                    {
+                        that._resetCounters(e)
+                    });
+
+                    const columnsDefinition = [
+                        {
+                            label: 'Identity',
+                            field: "identity",
+                            width: "150",
+                            id: "identity"
+                        },
+                        {
+                            label: 'Port',
+                            field: "port",
+                            width: "100",
+                            id: "port"
+                        },
+                        {
+                            label: 'Blocked',
+                            field: "blocked",
+                            width: "50",
+                            id: "blocked"
+                        },
+                        {
+                            label: 'Count Limit',
+                            field: "countLimit",
+                            width: "100",
+                            id: "count"
+                        },
+                        {
+                            label: 'Frequency Limit',
+                            field: "frequencyLimit",
+                            width: "100",
+                            id: "frequency"
+                        },
+                        {
+                            label: 'Frequency period [ms]',
+                            field: "frequencyPeriod",
+                            width: "100",
+                            id: "period"
+                        }
+                    ];
+
+                    const gridFactory = declare([Grid, Pagination, ColumnResizer]);
+                    that._ruleGrid = gridFactory(
+                        {
+                            collection: that._dataSore,
+                            rowsPerPage: 10,
+                            firstLastArrows: true,
+                            selectionMode: 'none',
+                            className: 'dgrid-autoheight',
+                            pageSizeOptions: [10, 25, 50, 100],
+                            columns: columnsDefinition,
+                            noDataMessage: 'No connection limit rules.'
+                        }, query(".rules", node)[0]);
+                });
+        }
+
+        ConnectionLimitFile.prototype.update = function (data)
+        {
+            this._path.innerHTML = entities.encode(String(data.path));
+            this._frequencyPeriod.innerHTML = entities.encode(String(data.defaultFrequencyPeriod));
+            this._dataSore.setData(data.rules);
+            if (Array.isArray(data.rules) && data.rules.length > 0)
+            {
+                this._ruleGrid.refresh(this._refreshOption || {});
+                this._refreshOption = {keepCurrentPage: true, keepScrollPosition: true};
+            }
+            else
+            {
+                this._ruleGrid.refresh();
+                this._refreshOption = {};
+            }
+        };
+
+        ConnectionLimitFile.prototype._reload = function (e)
+        {
+            event.stop(e);
+            this._reloadButton.set("disabled", true);
+            this._resetButton.set("disabled", true);
+            const modelObj = {
+                type: this.modelObj.type,
+                name: "reload",
+                parent: this.modelObj
+            };
+            const url = this.management.buildObjectURL(modelObj);
+            const that = this;
+            this.management.post({url: url}, {})
+                .then(null, management.xhrErrorHandler)
+                .always(function ()
+                {
+                    that._reloadButton.set("disabled", false);
+                    that._resetButton.set("disabled", false);
+                });
+        };
+
+        ConnectionLimitFile.prototype._resetCounters = function (e)
+        {
+            event.stop(e);
+            if (confirm("Are you sure you want to reset all connection counters?"))
+            {
+                this._resetButton.set("disabled", true);
+                this._reloadButton.set("disabled", true);
+                const modelObj = {
+                    type: this.modelObj.type,
+                    name: "resetCounters",
+                    parent: this.modelObj
+                };
+                const url = this.management.buildObjectURL(modelObj);
+                const that = this;
+                this.management.post({url: url}, {})
+                    .then(null, management.xhrErrorHandler)
+                    .always(function ()
+                    {
+                        that._resetButton.set("disabled", false);
+                        that._reloadButton.set("disabled", false);
+                    });
+            }
+        };
+
+        return ConnectionLimitFile;
+    });
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/rulebased/add.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/rulebased/add.js
new file mode 100644
index 0000000..686ef9e
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/rulebased/add.js
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+define(["dojo/dom",
+        "qpid/common/util",
+        "dijit/form/NumberTextBox"],
+    function (dom, util)
+    {
+        return {
+            show: function (data)
+            {
+                util.parseHtmlIntoDiv(data.containerNode,
+                    "connectionlimitprovider/rulebased/add.html",
+                    function ()
+                    {
+                        util.applyToWidgets(data.containerNode,
+                            data.category,
+                            data.type,
+                            data.initialData,
+                            data.metadata,
+                            data.effectiveData);
+                    });
+            }
+        };
+    });
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/rulebased/load.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/rulebased/load.js
new file mode 100644
index 0000000..27d8792
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/rulebased/load.js
@@ -0,0 +1,108 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+define(["dojo/parser",
+        "dojo/dom-construct",
+        "dijit/registry",
+        "dojo/_base/event",
+        "qpid/common/util",
+        "dojo/text!connectionlimitprovider/rulebased/load.html",
+        "qpid/common/ResourceWidget",
+        "dijit/Dialog",
+        "dijit/form/Button",
+        "dijit/form/Form",
+        "dojo/domReady!"],
+    function (parser,
+              construct,
+              registry,
+              event,
+              util,
+              template)
+    {
+        function Load()
+        {
+            const that = this;
+            this.containerNode = construct.create("div", {innerHTML: template});
+
+            parser.parse(this.containerNode)
+                .then(function ()
+                {
+                    that._dialog = registry.byId("loadConnectionLimitProvider.ruleBased");
+                    that._form = registry.byId("loadConnectionLimitProvider.form");
+
+                    const submitButton = registry.byId("loadConnectionLimitProvider.submitButton");
+                    submitButton.on("click", function (e)
+                    {
+                        that._submit(submitButton, e);
+                    });
+
+                    const cancelButton = registry.byId("loadConnectionLimitProvider.cancelButton");
+                    cancelButton.on("click", function (e)
+                    {
+                        event.stop(e);
+                        that._dialog.hide();
+                    });
+
+                    if (!window.FileReader)
+                    {
+                        const oldBrowserWarning = registry.byId("loadConnectionLimitProvider.oldBrowserWarning");
+                        oldBrowserWarning.innerHTML = "File upload requires a more recent browser with HTML5 support";
+                        oldBrowserWarning.className = oldBrowserWarning.className.replace("hidden", "");
+                    }
+                });
+        }
+
+        Load.prototype.show = function (management, modelObj)
+        {
+            this.management = management;
+            this.modelObj = modelObj;
+            this._form.reset();
+            this._dialog.show();
+        };
+
+        Load.prototype._submit = function (submitButton, e)
+        {
+            event.stop(e);
+            if (this._form.validate())
+            {
+                submitButton.set("disabled", true);
+                const that = this;
+                const modelObj = {
+                    type: this.modelObj.type,
+                    name: "loadFromFile",
+                    parent: this.modelObj
+                };
+                const url = {url: this.management.buildObjectURL(modelObj)};
+                const data = util.getFormWidgetValues(this._form, {});
+                this.management.post(url, data)
+                    .then(that._dialog.hide.bind(that._dialog), this.management.xhrErrorHandler)
+                    .always(function ()
+                    {
+                        submitButton.set("disabled", false);
+                    });
+            }
+            else
+            {
+                alert('Form contains invalid data. Please correct first');
+            }
+        };
+
+        return new Load();
+    });
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/rulebased/rule.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/rulebased/rule.js
new file mode 100644
index 0000000..2067fee
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/rulebased/rule.js
@@ -0,0 +1,185 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+define(["dojo/parser",
+        "dojo/dom-construct",
+        "dijit/registry",
+        "dojo/_base/event",
+        "qpid/common/util",
+        "dojo/text!connectionlimitprovider/rulebased/rule.html",
+        "dijit/Dialog",
+        "dijit/form/Button",
+        "dijit/form/Form",
+        "dijit/form/TextBox",
+        "dijit/form/ValidationTextBox",
+        "dijit/form/NumberTextBox",
+        "dijit/form/CheckBox",
+        "dojo/domReady!"],
+    function (parser,
+              construct,
+              registry,
+              event,
+              util,
+              template)
+    {
+        function Rule()
+        {
+            const that = this;
+            this.containerNode = construct.create("div", {innerHTML: template});
+
+            parser.parse(this.containerNode)
+                .then(function ()
+                {
+                    that._dialog = registry.byId("connectionLimitRule");
+                    that._form = registry.byId("connectionLimitRule.form");
+                    that._formBox = util.findNode("formBox", that.containerNode)[0];
+
+                    that._saveButton = registry.byId("connectionLimitRule.saveButton");
+                    that._saveButton.on("click", function (e)
+                    {
+                        that._saveRule(e);
+                    });
+                    that._deleteButton = registry.byId("connectionLimitRule.deleteButton");
+                    that._deleteButton.on("click", function (e)
+                    {
+                        that._deleteRule(e);
+                    });
+                    that._cancelButton = registry.byId("connectionLimitRule.cancelButton");
+                    that._cancelButton.on("click", function (e)
+                    {
+                        that._cancel(e);
+                    });
+                });
+        }
+
+        Rule.prototype.show = function (data)
+        {
+            this.management = data.management;
+            this.modelObj = data.modelObj;
+
+            this._gridData = [];
+            this._isNew = true;
+            let current = {id: undefined};
+
+            for (const rule of data.gridData)
+            {
+                if (data.id === rule.id)
+                {
+                    this._gridData.push(Object.assign(current, rule));
+                    this._isNew = false;
+                }
+                else
+                {
+                    this._gridData.push(rule);
+                }
+            }
+            this._currentRule = current;
+
+            this._form.reset();
+            util.applyToWidgets(this._formBox,
+                data.category,
+                data.type,
+                current,
+                data.management.metadata,
+                {});
+
+            this._dialog.show();
+        };
+
+        Rule.prototype._cancel = function (e)
+        {
+            event.stop(e);
+            this._dialog.hide();
+        };
+
+        Rule.prototype._saveRule = function (e)
+        {
+            event.stop(e);
+            if (this._form.validate())
+            {
+                this._deleteButton.set("disabled", true);
+                this._saveButton.set("disabled", true);
+
+                delete this._currentRule.port;
+                delete this._currentRule.blocked;
+                delete this._currentRule.countLimit;
+                delete this._currentRule.frequencyLimit;
+                delete this._currentRule.frequencyPeriod;
+
+                Object.assign(this._currentRule, util.getFormWidgetValues(this._form, {}));
+                if (this._isNew)
+                {
+                    this._gridData.push(this._currentRule);
+                    this._isNew = false;
+                }
+                this._submit();
+            }
+            else
+            {
+                alert('Form contains invalid data. Please correct first');
+            }
+        };
+
+        Rule.prototype._deleteRule = function (e)
+        {
+            event.stop(e);
+            if (!this._isNew)
+            {
+                this._deleteButton.set("disabled", true);
+                this._saveButton.set("disabled", true);
+                let i = 0;
+                while (i < this._gridData.length)
+                {
+                    if (this._gridData[i].id === this._currentRule.id)
+                    {
+                        this._gridData.splice(i, 1);
+                    }
+                    else
+                    {
+                        i++;
+                    }
+                }
+                this._isNew = true;
+                this._submit();
+            }
+        };
+
+        Rule.prototype._submit = function ()
+        {
+            const data = [];
+            for (const rule of this._gridData)
+            {
+                const r = Object.assign({}, rule);
+                delete r.id;
+                data.push(r);
+            }
+
+            const that = this;
+            this.management.update(this.modelObj, {rules: data})
+                .then(that._dialog.hide.bind(that._dialog), this.management.xhrErrorHandler)
+                .always(function ()
+                {
+                    that._deleteButton.set("disabled", false);
+                    that._saveButton.set("disabled", false);
+                });
+        }
+
+        return new Rule();
+    });
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/rulebased/show.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/rulebased/show.js
new file mode 100644
index 0000000..1f5ce77
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/connectionlimitprovider/rulebased/show.js
@@ -0,0 +1,281 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+define(["dojo/_base/declare",
+        "dojo/_base/connect",
+        "dojo/_base/event",
+        "dojo/parser",
+        "dojo/query",
+        "dojo/dom-construct",
+        "dijit/registry",
+        "dojox/html/entities",
+        "dgrid/Grid",
+        "dgrid/Selection",
+        "dgrid/extensions/Pagination",
+        "dgrid/extensions/ColumnResizer",
+        "dgrid/extensions/ColumnHider",
+        "dstore/Memory",
+        "qpid/management/connectionlimitprovider/rulebased/load",
+        "qpid/management/connectionlimitprovider/rulebased/rule",
+        "dojo/text!connectionlimitprovider/rulebased/show.html",
+        "dijit/TitlePane",
+        "dijit/form/Button",
+        "dojo/domReady!"],
+    function (declare,
+              connect,
+              event,
+              parser,
+              query,
+              construct,
+              registry,
+              entities,
+              Grid,
+              Selection,
+              Pagination,
+              ColumnResizer,
+              ColumnHider,
+              MemoryStore,
+              loadForm,
+              ruleForm,
+              template)
+    {
+        function RuleBased(containerNode, conLimitModelObj, controller)
+        {
+            const that = this;
+            this.modelObj = conLimitModelObj;
+            this.management = controller.management;
+            this.category =
+                conLimitModelObj &&
+                (conLimitModelObj.type === "virtualhost" ||
+                    conLimitModelObj.type === "virtualhostconnectionlimitprovider")
+                    ? "VirtualHostConnectionLimitProvider"
+                    : "BrokerConnectionLimitProvider";
+            this._loadForm = loadForm;
+            this._ruleForm = ruleForm;
+
+            this._dataSore = new MemoryStore({data: [], idProperty: "id"});
+
+            const node = construct.create("div", {innerHTML: template}, containerNode, "last");
+            parser.parse(containerNode)
+                .then(function ()
+                {
+                    that._frequencyPeriod = query(".defaultFrequencyPeriod", node)[0];
+
+                    const addButton = registry.byNode(query(".addRule", node)[0]);
+                    addButton.on("click", function (e)
+                    {
+                        event.stop(e);
+                        that._ruleForm.show({
+                            management: that.management,
+                            modelObj: that.modelObj,
+                            gridData: that.gridData,
+                            type: that.type,
+                            category: that.category
+                        });
+                    });
+
+                    that._resetButton = registry.byNode(query(".resetCounters", node)[0]);
+                    that._resetButton.on("click", function (e)
+                    {
+                        that._resetCounters(e)
+                    });
+
+                    that._clearButton = registry.byNode(query(".clearRules", node)[0]);
+                    that._clearButton.on("click", function (e)
+                    {
+                        that._clear(e)
+                    });
+
+                    const loadButton = registry.byNode(query(".loadRules", node)[0]);
+                    loadButton.on("click", function (e)
+                    {
+                        event.stop(e);
+                        that._loadForm.show(that.management, that.modelObj);
+                    });
+
+                    const extractButton = registry.byNode(query(".extractRules", node)[0]);
+                    extractButton.on("click", function (e)
+                    {
+                        event.stop(e);
+                        that.management.downloadIntoFrame({
+                            type: that.modelObj.type,
+                            name: "extractRules",
+                            parent: that.modelObj
+                        });
+                    });
+
+                    const columnsDefinition = [
+                        {
+                            label: 'ID',
+                            field: "id",
+                            width: "30",
+                            id: "ID",
+                            hidden: true,
+                        },
+                        {
+                            label: 'Identity',
+                            field: "identity",
+                            width: "150",
+                            id: "identity",
+                            hidden: false,
+                        },
+                        {
+                            label: 'Port',
+                            field: "port",
+                            width: "100",
+                            id: "port",
+                            hidden: false,
+                        },
+                        {
+                            label: 'Blocked',
+                            field: "blocked",
+                            width: "50",
+                            id: "blocked",
+                            hidden: false,
+                        },
+                        {
+                            label: 'Count Limit',
+                            field: "countLimit",
+                            width: "100",
+                            id: "count",
+                            hidden: false,
+                        },
+                        {
+                            label: 'Frequency Limit',
+                            field: "frequencyLimit",
+                            width: "100",
+                            id: "frequency",
+                            hidden: false,
+                        },
+                        {
+                            label: 'Frequency period [ms]',
+                            field: "frequencyPeriod",
+                            width: "100",
+                            id: "period",
+                            hidden: false,
+                        }
+                    ];
+
+                    const gridFactory = declare([Grid, Selection, Pagination, ColumnResizer, ColumnHider]);
+                    that._ruleGrid = gridFactory(
+                        {
+                            collection: that._dataSore,
+                            rowsPerPage: 10,
+                            pagingLinks: 1,
+                            firstLastArrows: true,
+                            selectionMode: 'single',
+                            className: 'dgrid-autoheight',
+                            pageSizeOptions: [10, 25, 50, 100],
+                            columns: columnsDefinition,
+                            noDataMessage: 'No connection limit rules.'
+                        }, query(".rules", node)[0]);
+
+                    that._ruleGrid.on('dgrid-select', function (evn) {
+                        const theItem = evn.rows[0].data;
+                        that._ruleForm.show({
+                            management: that.management,
+                            modelObj: that.modelObj,
+                            gridData: that.gridData,
+                            id: theItem.id,
+                            type: that.type,
+                            category: that.category
+                        });
+                    });
+                });
+        }
+
+        RuleBased.prototype.update = function (data)
+        {
+            this.type = data.type;
+            this._frequencyPeriod.innerHTML = entities.encode(String(data.defaultFrequencyPeriod));
+
+            const gridData = [];
+            let i = 0;
+            for (const rule of data.rules)
+            {
+                const newRule = {id: i};
+                Object.assign(newRule, rule);
+                gridData.push(newRule);
+                i++;
+            }
+            this.gridData = gridData;
+            this._dataSore.setData(gridData);
+            if (Array.isArray(gridData) && gridData.length > 0)
+            {
+                this._ruleGrid.refresh(this._refreshOption || {});
+                this._refreshOption = {keepCurrentPage: true, keepScrollPosition: true};
+            }
+            else
+            {
+                this._ruleGrid.refresh();
+                this._refreshOption = {};
+            }
+        };
+
+        RuleBased.prototype._clear = function (e)
+        {
+            event.stop(e);
+            if (confirm("Are you sure you want to clear all connection limit rules?"))
+            {
+                this._clearButton.set("disabled", true);
+                this._resetButton.set("disabled", true);
+                const modelObj = {
+                    type: this.modelObj.type,
+                    name: "clearRules",
+                    parent: this.modelObj
+                };
+                const url = {url: this.management.buildObjectURL(modelObj)};
+
+                const that = this;
+                this.management.post(url, {})
+                    .always(function ()
+                    {
+                        that._clearButton.set("disabled", false);
+                        that._resetButton.set("disabled", false);
+                    });
+            }
+        };
+
+        RuleBased.prototype._resetCounters = function (e)
+        {
+            event.stop(e);
+            if (confirm("Are you sure you want to reset all connection counters?"))
+            {
+                this._resetButton.set("disabled", true);
+                this._clearButton.set("disabled", true);
+                const modelObj = {
+                    type: this.modelObj.type,
+                    name: "resetCounters",
+                    parent: this.modelObj
+                };
+                const url = {url: this.management.buildObjectURL(modelObj)};
+
+                const that = this;
+                this.management.post(url, {})
+                    .always(function ()
+                    {
+                        that._resetButton.set("disabled", false);
+                        that._clearButton.set("disabled", false);
+                    });
+            }
+        };
+
+        return RuleBased;
+    });
diff --git a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/controller.js b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/controller.js
index 83a152c..677f0b5 100644
--- a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/controller.js
+++ b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/controller.js
@@ -44,6 +44,7 @@
         "qpid/management/QueryBrowserTab",
         "qpid/management/DashboardTab",
         "qpid/management/DashboardBrowserTab",
+        "qpid/management/ConnectionLimitProvider",
         "qpid/common/util",
         "dojo/ready",
         "dojox/uuid/generateRandomUuid",
@@ -74,6 +75,7 @@
               QueryBrowserTab,
               DashboardTab,
               DashboardBrowserTab,
+              ConnectionLimitProvider,
               util,
               ready)
     {
@@ -100,7 +102,9 @@
             query: QueryTab,
             dashboard: DashboardTab,
             queryBrowser: QueryBrowserTab,
-            dashboardBrowser: DashboardBrowserTab
+            dashboardBrowser: DashboardBrowserTab,
+            brokerconnectionlimitprovider: ConnectionLimitProvider,
+            virtualhostconnectionlimitprovider: ConnectionLimitProvider
         };
 
         ready(function ()
diff --git a/broker-plugins/management-http/src/main/java/resources/showBroker.html b/broker-plugins/management-http/src/main/java/resources/showBroker.html
index 956e415..713543c 100644
--- a/broker-plugins/management-http/src/main/java/resources/showBroker.html
+++ b/broker-plugins/management-http/src/main/java/resources/showBroker.html
@@ -142,6 +142,11 @@
         <button data-dojo-type="dijit.form.Button" class="addAccessControlProvider">Add Access Control Provider</button>
         <button data-dojo-type="dijit.form.Button" class="deleteAccessControlProvider">Delete Access Control Provider</button>
     </div>
+    <div data-dojo-type="dijit.TitlePane" data-dojo-props="title: 'Connection Limit Providers'">
+        <div class="broker-connection-limit-providers"></div>
+        <button data-dojo-type="dijit.form.Button" class="addConnectionLimitProvider">Add Connection Limit Provider</button>
+        <button data-dojo-type="dijit.form.Button" class="deleteConnectionLimitProvider">Delete Connection Limit Provider</button>
+    </div>
     <br/>
 </div>
 
diff --git a/broker-plugins/management-http/src/main/java/resources/showConnectionLimitProvider.html b/broker-plugins/management-http/src/main/java/resources/showConnectionLimitProvider.html
new file mode 100644
index 0000000..09aa710
--- /dev/null
+++ b/broker-plugins/management-http/src/main/java/resources/showConnectionLimitProvider.html
@@ -0,0 +1,47 @@
+<!--
+ -
+ - Licensed to the Apache Software Foundation (ASF) under one
+ - or more contributor license agreements.  See the NOTICE file
+ - distributed with this work for additional information
+ - regarding copyright ownership.  The ASF licenses this file
+ - to you under the Apache License, Version 2.0 (the
+ - "License"); you may not use this file except in compliance
+ - with the License.  You may obtain a copy of the License at
+ -
+ -   http://www.apache.org/licenses/LICENSE-2.0
+ -
+ - Unless required by applicable law or agreed to in writing,
+ - software distributed under the License is distributed on an
+ - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ - KIND, either express or implied.  See the License for the
+ - specific language governing permissions and limitations
+ - under the License.
+ -
+ -->
+<div>
+    <div class="connectionLimitProvider" data-dojo-type="dijit.TitlePane"
+         data-dojo-props="title: 'User Connection Limit Provider Attributes', open: true">
+        <div class="clear">
+            <div class="formLabel-labelCell">Name:</div>
+            <div class="formValue-valueCell name"></div>
+        </div>
+        <div class="clear">
+            <div class="formLabel-labelCell">Type:</div>
+            <div class="formValue-valueCell type"></div>
+        </div>
+        <div class="clear">
+            <div class="formLabel-labelCell">State:</div>
+            <div class="formValue-valueCell state"></div>
+        </div>
+        <div class="providerDetails"></div>
+        <div class="clear">
+            <div class="dijitDialogPaneActionBar">
+                <input class="deleteConnectionLimitProviderButton"
+                       type="button"
+                       value="Delete User Connection Limit provider"
+                       label="Delete User Connection Limit Provider"
+                       dojoType="dijit.form.Button"/>
+            </div>
+        </div>
+    </div>
+</div>
diff --git a/broker-plugins/management-http/src/main/java/resources/showVirtualHost.html b/broker-plugins/management-http/src/main/java/resources/showVirtualHost.html
index 62bb2fd..217d43f 100644
--- a/broker-plugins/management-http/src/main/java/resources/showVirtualHost.html
+++ b/broker-plugins/management-http/src/main/java/resources/showVirtualHost.html
@@ -150,6 +150,16 @@
                     class="deleteVirtualHostAccessControlProvider">Delete Access Control Provider</button>
         </div>
         <br/>
+        <div data-dojo-type="dijit.TitlePane"
+             data-dojo-props="title: 'Virtual Host Connection Limit Providers'"
+             class="connectionLimitProviders">
+            <div class="virtualHostConnectionLimitProviders"></div>
+            <button data-dojo-type="dijit.form.Button"
+                    class="addVirtualHostConnectionLimitProvider">Add Connection Limit Provider</button>
+            <button data-dojo-type="dijit.form.Button"
+                    class="deleteVirtualHostConnectionLimitProvider">Delete Connection Limit Provider</button>
+        </div>
+        <br/>
     </div>
 </div>
 
diff --git a/broker/pom.xml b/broker/pom.xml
index 3ec8a4e..ffc94e2 100644
--- a/broker/pom.xml
+++ b/broker/pom.xml
@@ -57,6 +57,12 @@
 
     <dependency>
       <groupId>org.apache.qpid</groupId>
+      <artifactId>qpid-broker-plugins-connection-limits</artifactId>
+      <scope>runtime</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.qpid</groupId>
       <artifactId>qpid-broker-plugins-amqp-0-8-protocol</artifactId>
       <scope>runtime</scope>
     </dependency>
diff --git a/doc/java-broker/src/docbkx/Java-Broker-Initial-Configuration.xml b/doc/java-broker/src/docbkx/Java-Broker-Initial-Configuration.xml
index 6f17109..981838f 100644
--- a/doc/java-broker/src/docbkx/Java-Broker-Initial-Configuration.xml
+++ b/doc/java-broker/src/docbkx/Java-Broker-Initial-Configuration.xml
@@ -51,6 +51,9 @@
                     <para>Access Control Providers</para>
                 </listitem>
                 <listitem>
+                    <para>User Connection Limit Providers</para>
+                </listitem>
+                <listitem>
                     <para>Group Providers (optionally with Groups and GroupMembers for managing groups Group Providers)</para>
                 </listitem>
                 <listitem>
diff --git a/doc/java-broker/src/docbkx/Java-Broker-Management-Managing-Entities.xml b/doc/java-broker/src/docbkx/Java-Broker-Management-Managing-Entities.xml
index 7c4352d..dc1a544 100644
--- a/doc/java-broker/src/docbkx/Java-Broker-Management-Managing-Entities.xml
+++ b/doc/java-broker/src/docbkx/Java-Broker-Management-Managing-Entities.xml
@@ -93,5 +93,6 @@
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="management/managing/Java-Broker-Management-Managing-Truststores.xml"/>
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="management/managing/Java-Broker-Management-Managing-Group-Providers.xml"/>
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="management/managing/Java-Broker-Management-Managing-Access-Control-Providers.xml"/>
+  <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="management/managing/Java-Broker-Management-Managing-Connection-Limit-Providers.xml"/>
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="management/managing/Java-Broker-Management-Managing-Plugins-HTTP.xml"/>
 </chapter>
diff --git a/doc/java-broker/src/docbkx/Java-Broker-Security.xml b/doc/java-broker/src/docbkx/Java-Broker-Security.xml
index a7e389e..b3f268a 100644
--- a/doc/java-broker/src/docbkx/Java-Broker-Security.xml
+++ b/doc/java-broker/src/docbkx/Java-Broker-Security.xml
@@ -25,5 +25,6 @@
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="security/Java-Broker-Security-Authentication-Providers.xml"/>
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="security/Java-Broker-Security-Group-Providers.xml"/>
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="security/Java-Broker-Security-AccessControlProviders.xml"/>
+  <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="security/Java-Broker-Security-ConnectionLimitProviders.xml"/>
   <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="security/Java-Broker-Security-Configuration-Encryption.xml"/>
 </chapter>
diff --git a/doc/java-broker/src/docbkx/concepts/Java-Broker-Concepts-Other-Services.xml b/doc/java-broker/src/docbkx/concepts/Java-Broker-Concepts-Other-Services.xml
index 897fb40..dc0a9b1 100644
--- a/doc/java-broker/src/docbkx/concepts/Java-Broker-Concepts-Other-Services.xml
+++ b/doc/java-broker/src/docbkx/concepts/Java-Broker-Concepts-Other-Services.xml
@@ -23,7 +23,8 @@
 <section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="Java-Broker-Concepts-Other-Services">
 <title>Other Services</title>
     <para>
-        The Broker can also have <emphasis>Access Control Providers</emphasis>, <emphasis>Group Providers</emphasis>,
+        The Broker can also have <emphasis>Access Control Providers</emphasis>,
+        <emphasis>Connection Limit Providers</emphasis>, <emphasis>Group Providers</emphasis>,
         <emphasis>Keystores</emphasis>, <emphasis>Trustores</emphasis> and [Management] <emphasis>Plugins</emphasis> configured.
     </para>
 
@@ -33,6 +34,14 @@
         <para>Access Control Provider configuration and management details are covered in <xref linkend="Java-Broker-Security-AccessControlProviders"/>.</para>
     </section>
 
+    <section xml:id="Java-Broker-Concepts-Connection-Limit-Providers">
+        <title>Connection Limit Providers</title>
+        <para><emphasis>Connection Limit Providers</emphasis> are used to limit the amount of connections that
+            user could open on AMQP ports.</para>
+        <para>Connection Limit Providers configuration and management details are covered in
+            <xref linkend="Java-Broker-Security-ConnectionLimitProviders"/>.</para>
+    </section>
+
     <section xml:id="Java-Broker-Concepts-Group-Providers">
         <title>Group Providers</title>
         <para><emphasis>Group Providers</emphasis> are used to aggregate authenticated user principals into groups
diff --git a/doc/java-broker/src/docbkx/concepts/Java-Broker-Concepts-Overview.xml b/doc/java-broker/src/docbkx/concepts/Java-Broker-Concepts-Overview.xml
index 714de6c..372072e 100644
--- a/doc/java-broker/src/docbkx/concepts/Java-Broker-Concepts-Overview.xml
+++ b/doc/java-broker/src/docbkx/concepts/Java-Broker-Concepts-Overview.xml
@@ -51,6 +51,8 @@
     Broker supports zero or more group providers.</para>
   <para><emphasis>Access Control Provider</emphasis> allows the abilities of users (or groups of
     users) to be restrained. A Broker can have zero or one access control providers.</para>
+  <para><emphasis>Connection Limit Provider</emphasis> restrains users (or groups of
+    users) at opening new connections on AMQP ports.</para>
   <para><emphasis>Keystores</emphasis> provide a repositories of certificates and are used when the
     Broker accepts SSL connections. Any number of keystore providers can be defined. Keystores are
     be associated with Ports defined to accepts SSL.</para>
diff --git a/doc/java-broker/src/docbkx/management/managing/Java-Broker-Management-Managing-Broker.xml b/doc/java-broker/src/docbkx/management/managing/Java-Broker-Management-Managing-Broker.xml
index a4cae77..e0d8a51 100644
--- a/doc/java-broker/src/docbkx/management/managing/Java-Broker-Management-Managing-Broker.xml
+++ b/doc/java-broker/src/docbkx/management/managing/Java-Broker-Management-Managing-Broker.xml
@@ -108,6 +108,9 @@
         <listitem>
           <para>Access Control Providers</para>
         </listitem>
+        <listitem>
+          <para>Connection Limit Providers</para>
+        </listitem>
       </itemizedlist>
     </para>
   </section>
diff --git a/doc/java-broker/src/docbkx/management/managing/Java-Broker-Management-Managing-Connection-Limit-Providers.xml b/doc/java-broker/src/docbkx/management/managing/Java-Broker-Management-Managing-Connection-Limit-Providers.xml
new file mode 100644
index 0000000..aa03a39
--- /dev/null
+++ b/doc/java-broker/src/docbkx/management/managing/Java-Broker-Management-Managing-Connection-Limit-Providers.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+-->
+
+<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="Java-Broker-Management-Managing-Connection-Limit-Providers">
+    <title>Connection Limit Providers</title>
+    <para>An <link linkend="Java-Broker-Concepts-Connection-Limit-Providers">Connection Limit Provider</link>
+        governs how many connections could user open on AMQP ports.</para>
+    <para>See <xref linkend="Java-Broker-Security-ConnectionLimitProviders"/></para>
+</section>
diff --git a/doc/java-broker/src/docbkx/security/Java-Broker-Security-AccessControlProviders.xml b/doc/java-broker/src/docbkx/security/Java-Broker-Security-AccessControlProviders.xml
index b67df97..8b92dd2 100644
--- a/doc/java-broker/src/docbkx/security/Java-Broker-Security-AccessControlProviders.xml
+++ b/doc/java-broker/src/docbkx/security/Java-Broker-Security-AccessControlProviders.xml
@@ -428,36 +428,6 @@
           </entry>
         </row>
         <row>
-          <entry><command>connection_limit</command></entry>
-          <entry>
-            <para>
-              A maximum number of connections the users belonging to the given identity can establish to the Virtual Host
-            </para>
-            <para>
-              Intended for use in ACCESS VIRTUALHOST rules to restrict the number of connections which can be made
-              by the messaging user.
-            </para>
-          </entry>
-        </row>
-        <row>
-          <entry><command>connection_frequency_limit</command></entry>
-          <entry>
-            <para>
-              A maximum number of connections the user belonging to the given identity can establish to the Virtual Host
-              within pre-defined period of time, which is 1 minute by default.
-            </para>
-            <para>
-              Intended for use in ACCESS VIRTUALHOST rules to restrict the frequency of connections which can be made
-              by the messaging user.
-            </para>
-            <para>If required, the frequency period can be changed using context variable
-              <emphasis>qpid.virtualhost.connectionFrequencyPeriodInMillis</emphasis>. As name suggests, its value needs
-              to be specified in milliseconds. Setting it to zero or negative value turns off the
-              <command>connection_frequency</command> evaluation.
-            </para>
-          </entry>
-        </row>
-        <row>
           <entry><command>virtualhost_name</command></entry>
           <entry>
             <para>
@@ -521,28 +491,6 @@
 
       </example>
     </section>
-    <section role="h4" xml:id="Java-Broker-Security-AccessControlProviders-ResourceRestrictions">
-      <title>
-        Resource restrictions
-      </title>
-      <para>
-        The number of Virtual Host connections and frequency of connections can be restricted with
-        <emphasis>ACCESS</emphasis> rule.
-      </para>
-      <example>
-        <title>Restricting user connections to virtual host</title>
-        <programlisting><![CDATA[
-          ACL ALLOW-LOG guest ACCESS VIRTUALHOST connection_limit=1
-          ACL ALLOW-LOG messaging-users ACCESS VIRTUALHOST connection_frequency_limit=100
-          ...]]>
-        </programlisting>
-      </example>
-      <para>In the example above the maximum number of connections allowed to establish to the Virtual Host by user
-        <emphasis>guest</emphasis> is set to <emphasis>1</emphasis>. The maximum connection frequency for every user
-        belonging to the user group <emphasis>messaging-users</emphasis> is set to <emphasis>100</emphasis> (per minute,
-        by default).
-      </para>
-    </section>
     <section role="h4" xml:id="Java-Broker-Security-AccessControlProviders-WorkedExample2">
       <title>
         Worked example 2 - Simple Messaging
diff --git a/doc/java-broker/src/docbkx/security/Java-Broker-Security-ConnectionLimitProviders.xml b/doc/java-broker/src/docbkx/security/Java-Broker-Security-ConnectionLimitProviders.xml
new file mode 100644
index 0000000..1d2f615
--- /dev/null
+++ b/doc/java-broker/src/docbkx/security/Java-Broker-Security-ConnectionLimitProviders.xml
@@ -0,0 +1,248 @@
+<?xml version="1.0"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+-->
+
+<section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="Java-Broker-Security-ConnectionLimitProviders">
+    <title>Connection Limit Providers</title>
+    <para>
+        The Connection Limit Provider governs the limits of connections that an user can simultaneously open.
+    </para>
+    <para>There are two points within the hierarchy that enforce connection limits: the Broker itself and at each
+        Virtual Host. When a limit needs to be checked, every check point configured with a provider is consulted
+        for a decision. The example, when making a decision about the opening a new connection. If the Virtual Host is
+        configured with Connection Limit Provider then the limits are checked. Unless the connection is rejected,
+        the decision is delegated to the Connection Limit Provider configured at the Broker.
+    </para>
+    <para>Connection Limit Provider is configured with a set of CLT (connection limit) rules. The rules determine
+        the limit of open connections, how many connections can user open on the
+        <link linkend="Java-Broker-Concepts-Ports">AMQP Ports</link>.
+    </para>
+    <para>
+        CLT rules may be written in terms of user or group names. A rule written in terms of a group name applies to the
+        user if he is a member of that group. Groups information is obtained from the
+        <link linkend="Java-Broker-Security-Authentication-Providers">Authentication Providers</link>
+        and <link linkend="Java-Broker-Security-Group-Providers">Group Providers</link>. Writing CLT rules in terms of
+        user names is recommended.
+    </para>
+    <para>
+        The Connection Limit Providers can be configured using
+        <link linkend="Java-Broker-Management-Channel-REST-API">REST Management interfaces</link>
+        and <link linkend="Java-Broker-Management-Channel-Web-Console">Web Management Console</link>.
+    </para>
+    <section role="h3" xml:id="Java-Broker-Security-ConnectionLimitProviders-Types">
+        <title>Types</title>
+        <para>There are currently two types of Connection Limit Provider implementing CLT rules.
+            <itemizedlist>
+                <listitem>
+                    <para>
+                        <emphasis>RulesBased</emphasis>
+                        - a provider that stores the rule-set within the Broker's or VirtualHost's configuration.
+                    </para>
+                </listitem>
+                <listitem>
+                    <para>
+                        <para>
+                            <emphasis>ConnectionLimitFile</emphasis>
+                            - a provider that references an externally provided CLT file (or data url).
+                        </para>
+                    </para>
+                </listitem>
+            </itemizedlist>
+        </para>
+    </section>
+    <section role="h3" xml:id="Java-Broker-Security-ConnectionLimitProviders-Rules">
+        <title>
+            Connection Limit Rules
+        </title>
+        <para>An CLT rule is composed of an user or group identification, AMQP port name and connection limits.
+            Let's look at some example.
+        </para>
+        <programlisting>
+            # Limits simultaneously open connection by alice on brokerAmqp port up to 10.
+            CLT alice port=brokerAmqp connection_count=10
+        </programlisting>
+        <para>If there is multiple rules for given user (or group) then the rules are merge into a single most
+            restrictive rule.
+        </para>
+        <programlisting>
+            CLT alice port=brokerAmqp connection_count=10
+            CLT alice port=brokerAmqp connection_count=12 connection_frequency_count=60/1m
+            CLT alice port=brokerAmqp connection_frequency_count=100/1m
+        </programlisting>
+        <para>The previous rules will be merge into a single effective rule.</para>
+        <programlisting>
+            CLT alice port=brokerAmqp connection_count=10 connection_frequency_count=60/1m
+        </programlisting>
+        <para>The rules are applied in following order:</para>
+        <orderedlist numeration="arabic">
+            <listitem>
+                <para>The effective rule for given user.</para>
+            </listitem>
+            <listitem>
+                <para>The effective rule for given set of groups that user is a member of.</para>
+            </listitem>
+            <listitem>
+                <para>The default rule, a rule with the user ALL that matches any user.</para>
+            </listitem>
+        </orderedlist>
+        <para>At the first broker looks for a rule for given user. If any rule is not found then broker will look for
+            the group rules. If any group rule is not found then broker will look for a default rule. An user without
+            any rule is not restricted.
+        </para>
+    </section>
+    <section role="h4" xml:id="Java-Broker-Security-ConnectionLimitProviders-Syntax">
+        <title>
+            Syntax
+        </title>
+        <para>
+            Connection limit rules follow this syntax:
+        </para>
+        <programlisting>
+            CLT {&lt;user-name&gt;|&lt;group-name&gt;|ALL} [BLOCK] [port=&lt;AMQP-port-name&gt;|ALL] [property="&lt;property-value&gt;"]
+        </programlisting>
+        <para>
+            A rule with user name ALL is default rule. Likewise a rule with port=ALL is applied to all ports.
+            The parameter BLOCK is optional and marks user or group that is not allowed to connect on the port.
+        </para>
+        <para>
+            Comments may be introduced with the hash (#) character and are ignored. A line can be broken with the slash
+            (\) character.
+        </para>
+        <programlisting>
+            # A comment
+            CLT alice port=brokerAMQP connection_limit=10 # Also a comment
+            CLT mark port=brokerAMQP \ # A broken line
+            connection_limit=10 \
+            connection_frequency_limit=60/1m
+            CLT ALL BLOCK # A default rule
+        </programlisting>
+        <table xml:id="table-Java-Broker-Security-ConnectionLimitProviders-Syntax_properties">
+            <title>List of connection limit (CLT) properties</title>
+            <tgroup cols="2">
+                <colspec colnum="1" colname="name" colwidth="1*"/>
+                <colspec colnum="2" colname="description" colwidth="1*"/>
+                <tbody>
+                    <row>
+                        <entry>
+                            <command>connection_limit</command>
+                        </entry>
+                        <entry>
+                            <para>
+                                Integer. A maximum number of connections the messaging user can establish to the Virtual
+                                Host on AMQP port.
+                            </para>
+                            <para>
+                                Alternatives: connection-limit, connectionLimit.
+                            </para>
+                        </entry>
+                    </row>
+                    <row>
+                        <entry>
+                            <command>connection_frequency_limit</command>
+                        </entry>
+                        <entry>
+                            <para>
+                                A maximum number of connections the messaging user can establish to the Virtual Host
+                                on AMQP port within defined period of time, which is 1 minute by default.
+                                The connection frequency limit is specified in the format: limit/period, where time
+                                period is written as xHyMz.wS - x hours, y minutes and z.w seconds.
+                            </para>
+                            <para>
+                                In case of time period 1 hour/minute/second the digit 1 can be omitted,
+                                for example: 7200/H or 120/M or 2/S.
+                                (7200/H is not the same frequency limit as 120/H or 2/S).
+                            </para>
+                            <para>
+                                If the period is omitted then the default frequency period is used.
+                                If required, the default frequency period can be changed using CONFIG command.
+                                See an example below. Setting it to zero or negative value turns off the connection
+                                frequency evaluation.
+                            </para>
+                            <para>
+                                Alternatives: connection-frequency-limit, connectionFrequencyLimit.
+                            </para>
+                        </entry>
+                    </row>
+                    <row>
+                        <entry>
+                            <command>port</command>
+                        </entry>
+                        <entry>
+                            <para>
+                                String. The AMQP port name, ALL is the default value.
+                            </para>
+                        </entry>
+                    </row>
+                </tbody>
+            </tgroup>
+        </table>
+        <para>
+            The default time period for frequency limit can be set up with the <literal>CONFIG</literal> command.
+            Default frequency period is specified in ms.
+        </para>
+        <programlisting>
+            CONFIG default_frequency_period=60000
+        </programlisting>
+        <para>
+            default-frequency-period and defaultFrequencyPeriod are valid alternatives to the default_frequency_period.
+        </para>
+        <para>
+            The default frequency period may be specified as context variable
+            <literal>qpid.broker.connectionLimiter.frequencyPeriodInMillis</literal>.
+        </para>
+        <para>
+            The Broker logs rejected connections when an user breaks the limit. But the Broker could also log
+            the accepted connections with current counter value. The full logging could be turn on with
+            <literal>CONFIG</literal> command.
+        </para>
+        <programlisting>
+            CONFIG log_all=true default_frequency_period=60000
+        </programlisting>
+        <para>
+            log-all and logAll are valid alternatives to the log_all.
+        </para>
+    </section>
+    <section role="h4" xml:id="Java-Broker-Security-ConnectionLimitProviders-WorkedExample">
+        <title>
+            Worked Example
+        </title>
+        <para>
+            Here are some example of connection limits illustrating common use cases.
+        </para>
+        <para>
+            Suppose you wish to restrict two users: a user <literal>operator</literal> can establish at the most 50
+            connections on any port. A user <literal>publisher</literal> can establish 30 new connection per two minutes
+            but at the most 20 parallel connections on <literal>amqp</literal> port. Another users should be blocked.
+        </para>
+        <example>
+            <title>CLT file example</title>
+            <programlisting><![CDATA[
+          # Limit operator
+          CLT operator connection_limit=50
+          # Limit publisher
+          CLT publisher port=amqp connection_frequency_limit=30/2M connection_limit=20
+          # Block all users by default
+          CLT ALL BLOCK
+            ]]>
+            </programlisting>
+        </example>
+    </section>
+</section>
diff --git a/doc/java-broker/src/docbkx/security/Java-Broker-Security-Group-Providers.xml b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Group-Providers.xml
index 3737f1d..41d2b2e 100644
--- a/doc/java-broker/src/docbkx/security/Java-Broker-Security-Group-Providers.xml
+++ b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Group-Providers.xml
@@ -23,7 +23,8 @@
 <section xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="Java-Broker-Security-Group-Providers">
   <title>Group Providers</title>
   <para>
-    The Apache Qpid Broker-J utilises GroupProviders to allow assigning users to groups for use in <link linkend="Java-Broker-Security-AccessControlProviders">ACLs</link>.
+    The Apache Qpid Broker-J utilises GroupProviders to allow assigning users to groups for use in
+      <link linkend="Java-Broker-Security-AccessControlProviders">ACLs</link> or <link linkend="Java-Broker-Security-ConnectionLimitProviders">CLTs</link>.
     Following authentication by a given <link linkend="Java-Broker-Security-Authentication-Providers">Authentication Provider</link>,
     the configured Group Providers are consulted allowing the assignment of GroupPrincipals for a given authenticated user. Any number of
     Group Providers can be added into the Broker. All of them will be checked for the presence of the groups for a given authenticated user.
diff --git a/pom.xml b/pom.xml
index ff4f4ea..42ed538 100644
--- a/pom.xml
+++ b/pom.xml
@@ -169,6 +169,7 @@
     <module>broker</module>
     <module>apache-qpid-broker-j</module>
     <module>broker-plugins/access-control</module>
+    <module>broker-plugins/connection-limits</module>
     <module>broker-plugins/amqp-0-8-protocol</module>
     <module>broker-plugins/amqp-0-10-protocol</module>
     <module>broker-plugins/amqp-1-0-protocol</module>
@@ -292,6 +293,12 @@
 
       <dependency>
         <groupId>org.apache.qpid</groupId>
+        <artifactId>qpid-broker-plugins-connection-limits</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+
+      <dependency>
+        <groupId>org.apache.qpid</groupId>
         <artifactId>qpid-broker-plugins-jdbc-provider-bone</artifactId>
         <version>${project.version}</version>
       </dependency>
diff --git a/systests/qpid-systests-jms-core/pom.xml b/systests/qpid-systests-jms-core/pom.xml
index 1797eee..a208055 100644
--- a/systests/qpid-systests-jms-core/pom.xml
+++ b/systests/qpid-systests-jms-core/pom.xml
@@ -110,6 +110,12 @@
             <scope>runtime</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.qpid</groupId>
+            <artifactId>qpid-broker-plugins-connection-limits</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+
     </dependencies>
 
     <build>
diff --git a/systests/qpid-systests-jms_1.1/src/test/java/org/apache/qpid/systests/jms_1_1/extensions/acl/MessagingACLTest.java b/systests/qpid-systests-jms_1.1/src/test/java/org/apache/qpid/systests/jms_1_1/extensions/acl/MessagingACLTest.java
index d568372..cc7f6a1 100644
--- a/systests/qpid-systests-jms_1.1/src/test/java/org/apache/qpid/systests/jms_1_1/extensions/acl/MessagingACLTest.java
+++ b/systests/qpid-systests-jms_1.1/src/test/java/org/apache/qpid/systests/jms_1_1/extensions/acl/MessagingACLTest.java
@@ -51,7 +51,6 @@
 import javax.jms.TemporaryQueue;
 import javax.jms.TemporaryTopic;
 import javax.jms.TextMessage;
-import javax.naming.NamingException;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.collect.Sets;
@@ -77,7 +76,6 @@
 import org.apache.qpid.server.security.group.GroupProviderImpl;
 import org.apache.qpid.systests.JmsTestBase;
 
-
 public class MessagingACLTest extends JmsTestBase
 {
     private static final Logger LOGGER = LoggerFactory.getLogger(MessagingACLTest.class);
@@ -182,125 +180,6 @@
     }
 
     @Test
-    public void testAuthorizationWithConnectionLimit() throws Exception
-    {
-        final int connectionLimit = 2;
-        configureACL(String.format("ACL ALLOW-LOG %s ACCESS VIRTUALHOST connection_limit='%d'",
-                                   USER2,
-                                   connectionLimit));
-
-        final List<Connection> establishedConnections = new ArrayList<>();
-        try
-        {
-            establishConnections(connectionLimit, establishedConnections);
-
-            verifyConnectionEstablishmentFails(connectionLimit);
-
-            establishedConnections.remove(0).close();
-            getConnectionBuilder().setUsername(USER2).setPassword(USER2_PASSWORD).build().close();
-        }
-        finally
-        {
-            closeConnections(establishedConnections);
-        }
-    }
-
-    @Test
-    public void testAuthorizationWithConnectionFrequencyLimit() throws Exception
-    {
-        final int connectionFrequencyLimit = 1;
-        configureACL(String.format("ACL ALLOW-LOG %s ACCESS VIRTUALHOST connection_frequency_limit='%d'",
-                                   USER2,
-                                   connectionFrequencyLimit));
-
-        final List<Connection> establishedConnections = new ArrayList<>();
-        try
-        {
-            establishConnections(connectionFrequencyLimit, establishedConnections);
-
-            verifyConnectionEstablishmentFails(connectionFrequencyLimit);
-
-            establishedConnections.remove(0).close();
-
-            verifyConnectionEstablishmentFails(connectionFrequencyLimit);
-        }
-        finally
-        {
-            closeConnections(establishedConnections);
-        }
-    }
-
-    @Test
-    public void testAuthorizationWithConnectionLimitAndFrequencyLimit() throws Exception
-    {
-        final int connectionFrequencyLimit = 2;
-        final int connectionLimit = 3;
-        configureACL(String.format("ACL ALLOW-LOG %s ACCESS VIRTUALHOST connection_limit='%d' connection_frequency_limit='%d'",
-                                   USER2,
-                                   connectionLimit,
-                                   connectionFrequencyLimit));
-
-        final List<Connection> establishedConnections = new ArrayList<>();
-        try
-        {
-            establishConnections(connectionFrequencyLimit, establishedConnections);
-
-            verifyConnectionEstablishmentFails(connectionFrequencyLimit);
-
-            establishedConnections.remove(0).close();
-
-            verifyConnectionEstablishmentFails(connectionFrequencyLimit);
-        }
-        finally
-        {
-            closeConnections(establishedConnections);
-        }
-    }
-
-    private void establishConnections(final int connectionNumber, final List<Connection> establishedConnections)
-            throws NamingException, JMSException
-    {
-        for (int i = 0; i < connectionNumber; i++)
-        {
-            establishedConnections.add(getConnectionBuilder().setUsername(USER2)
-                                                             .setPassword(USER2_PASSWORD)
-                                                             .setClientId(getTestName() + i)
-                                                             .build());
-        }
-    }
-
-    private void closeConnections(final List<Connection> establishedConnections) throws JMSException
-    {
-        for (Connection c : establishedConnections)
-        {
-            c.close();
-        }
-    }
-
-    private void verifyConnectionEstablishmentFails(final int frequencyLimit) throws NamingException
-    {
-        try
-        {
-            final Connection connection = getConnectionBuilder().setUsername(USER2)
-                                                                .setPassword(USER2_PASSWORD)
-                                                                .setClientId(getTestName() + frequencyLimit)
-                                                                .build();
-            try
-            {
-                fail("Connection creation should fail due to exceeding limit");
-            }
-            finally
-            {
-                connection.close();
-            }
-        }
-        catch (JMSException e)
-        {
-            //pass
-        }
-    }
-
-    @Test
     public void testConsumeFromTempQueueSuccess() throws Exception
     {
         configureACL(String.format("ACL ALLOW-LOG %s ACCESS VIRTUALHOST", USER1),
diff --git a/systests/qpid-systests-jms_1.1/src/test/java/org/apache/qpid/systests/jms_1_1/extensions/connectionlimit/MessagingConnectionLimitTest.java b/systests/qpid-systests-jms_1.1/src/test/java/org/apache/qpid/systests/jms_1_1/extensions/connectionlimit/MessagingConnectionLimitTest.java
new file mode 100644
index 0000000..3e17d1d
--- /dev/null
+++ b/systests/qpid-systests-jms_1.1/src/test/java/org/apache/qpid/systests/jms_1_1/extensions/connectionlimit/MessagingConnectionLimitTest.java
@@ -0,0 +1,322 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.qpid.systests.jms_1_1.extensions.connectionlimit;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.jms.Connection;
+import javax.jms.JMSException;
+import javax.naming.NamingException;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Test;
+
+import org.apache.qpid.server.user.connection.limits.plugins.ConnectionLimitRule;
+import org.apache.qpid.systests.JmsTestBase;
+
+import static org.junit.Assert.fail;
+
+public class MessagingConnectionLimitTest extends JmsTestBase
+{
+    private static final String USER = "admin";
+    private static final String USER_SECRET = "admin";
+    private static final String RULE_BASED_VIRTUAL_HOST_USER_CONNECTION_LIMIT_PROVIDER =
+            "org.apache.qpid.RuleBasedVirtualHostConnectionLimitProvider";
+    private static final String FREQUENCY_PERIOD = "frequencyPeriod";
+    private static final String RULES = "rules";
+
+    @Test
+    public void testAuthorizationWithConnectionLimit() throws Exception
+    {
+        final int connectionLimit = 2;
+        configureCLT(new ConnectionLimitRule()
+        {
+            @Override
+            public String getPort()
+            {
+                return null;
+            }
+
+            @Override
+            public String getIdentity()
+            {
+                return USER;</