diff --git a/doc/source/cli/command-objects/limit.rst b/doc/source/cli/command-objects/limit.rst
new file mode 100644
index 0000000000..71cf2a420d
--- /dev/null
+++ b/doc/source/cli/command-objects/limit.rst
@@ -0,0 +1,128 @@
+=====
+limit
+=====
+
+Identity v3
+
+Limits are used to specify project-specific limits thresholds of resources.
+
+limit create
+------------
+
+Create a new limit
+
+.. program:: limit create
+.. code:: bash
+
+    openstack limit create
+        [--description <description>]
+        [--region <region>]
+        --project <project>
+        --service <service>
+        --resource-limit <resource-limit>
+        <resource-name>
+
+.. option:: --description <description>
+
+   Useful description of the limit or its purpose
+
+.. option:: --region <region>
+
+   Region that the limit should be applied to
+
+.. describe:: --project <project>
+
+   The project that the limit applies to (required)
+
+.. describe:: --service <service>
+
+   The service that is responsible for the resource being limited (required)
+
+.. describe:: --resource-limit <resource-limit>
+
+   The limit to apply to the project (required)
+
+.. describe:: <resource-name>
+
+   The name of the resource to limit (e.g. cores or volumes)
+
+limit delete
+------------
+
+Delete project-specific limit(s)
+
+.. program:: limit delete
+.. code:: bash
+
+    openstack limit delete
+        <limit-id> [<limit-id> ...]
+
+.. describe:: <limit-id>
+
+    Limit(s) to delete (ID)
+
+limit list
+----------
+
+List project-specific limits
+
+.. program:: limit list
+.. code:: bash
+
+    openstack limit list
+        [--service <service>]
+        [--resource-name <resource-name>]
+        [--region <region>]
+
+.. option:: --service <service>
+
+    The service to filter the response by (name or ID)
+
+.. option:: --resource-name <resource-name>
+
+    The name of the resource to filter the response by
+
+.. option:: --region <region>
+
+   The region name to filter the response by
+
+limit show
+----------
+
+Display details about a limit
+
+.. program:: limit show
+.. code:: bash
+
+    openstack limit show
+        <limit-id>
+
+.. describe:: <limit-id>
+
+   Limit to display (ID)
+
+limit set
+---------
+
+Update a limit
+
+.. program:: limit show
+.. code:: bash
+
+    openstack limit set
+        [--description <description>]
+        [--resource-limit <resource-limit>]
+        <limit-id>
+
+
+.. option:: --description <description>
+
+   Useful description of the limit or its purpose
+
+.. option:: --resource-limit <resource-limit>
+
+   The limit to apply to the project
+
+.. describe:: <limit-id>
+
+   Limit to update (ID)
diff --git a/openstackclient/identity/v3/limit.py b/openstackclient/identity/v3/limit.py
new file mode 100644
index 0000000000..c6f1cb1fb5
--- /dev/null
+++ b/openstackclient/identity/v3/limit.py
@@ -0,0 +1,238 @@
+#   Licensed 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.
+#
+
+"""Limits action implementations."""
+
+import logging
+
+from osc_lib.command import command
+from osc_lib import exceptions
+from osc_lib import utils
+import six
+
+from openstackclient.i18n import _
+from openstackclient.identity import common as common_utils
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateLimit(command.ShowOne):
+    _description = _("Create a limit")
+
+    def get_parser(self, prog_name):
+        parser = super(CreateLimit, self).get_parser(prog_name)
+        parser.add_argument(
+            '--description',
+            metavar='<description>',
+            help=_('Description of the limit'),
+        )
+        parser.add_argument(
+            '--region',
+            metavar='<region>',
+            help=_('Region for the limit to affect.'),
+        )
+        parser.add_argument(
+            '--project',
+            metavar='<project>',
+            required=True,
+            help=_('Project to associate the resource limit to'),
+        )
+        parser.add_argument(
+            '--service',
+            metavar='<service>',
+            required=True,
+            help=_('Service responsible for the resource to limit'),
+        )
+        parser.add_argument(
+            '--resource-limit',
+            metavar='<resource-limit>',
+            required=True,
+            type=int,
+            help=_('The resource limit for the project to assume'),
+        )
+        parser.add_argument(
+            'resource_name',
+            metavar='<resource-name>',
+            help=_('The name of the resource to limit'),
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        identity_client = self.app.client_manager.identity
+
+        project = common_utils.find_project(
+            identity_client, parsed_args.project
+        )
+        service = common_utils.find_service(
+            identity_client, parsed_args.service
+        )
+        region = None
+        if parsed_args.region:
+            region = utils.find_resource(
+                identity_client.regions, parsed_args.region
+            )
+
+        limit = identity_client.limits.create(
+            project,
+            service,
+            parsed_args.resource_name,
+            parsed_args.resource_limit,
+            description=parsed_args.description,
+            region=region
+        )
+
+        limit._info.pop('links', None)
+        return zip(*sorted(six.iteritems(limit._info)))
+
+
+class ListLimit(command.Lister):
+    _description = _("List limits")
+
+    def get_parser(self, prog_name):
+        parser = super(ListLimit, self).get_parser(prog_name)
+        parser.add_argument(
+            '--service',
+            metavar='<service>',
+            help=_('Service responsible for the resource to limit'),
+        )
+        parser.add_argument(
+            '--resource-name',
+            metavar='<resource-name>',
+            dest='resource_name',
+            help=_('The name of the resource to limit'),
+        )
+        parser.add_argument(
+            '--region',
+            metavar='<region>',
+            help=_('Region for the registered limit to affect.'),
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        identity_client = self.app.client_manager.identity
+
+        service = None
+        if parsed_args.service:
+            service = common_utils.find_service(
+                identity_client, parsed_args.service
+            )
+        region = None
+        if parsed_args.region:
+            region = utils.find_resource(
+                identity_client.regions, parsed_args.region
+            )
+
+        limits = identity_client.limits.list(
+            service=service,
+            resource_name=parsed_args.resource_name,
+            region=region
+        )
+
+        columns = (
+            'ID', 'Project ID', 'Service ID', 'Resource Name',
+            'Resource Limit', 'Description', 'Region ID'
+        )
+        return (
+            columns,
+            (utils.get_item_properties(s, columns) for s in limits),
+        )
+
+
+class ShowLimit(command.ShowOne):
+    _description = _("Display limit details")
+
+    def get_parser(self, prog_name):
+        parser = super(ShowLimit, self).get_parser(prog_name)
+        parser.add_argument(
+            'limit_id',
+            metavar='<limit-id>',
+            help=_('Limit to display (ID)'),
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        identity_client = self.app.client_manager.identity
+        limit = identity_client.limits.get(parsed_args.limit_id)
+        limit._info.pop('links', None)
+        return zip(*sorted(six.iteritems(limit._info)))
+
+
+class SetLimit(command.ShowOne):
+    _description = _("Update information about a limit")
+
+    def get_parser(self, prog_name):
+        parser = super(SetLimit, self).get_parser(prog_name)
+        parser.add_argument(
+            'limit_id',
+            metavar='<limit-id>',
+            help=_('Limit to update (ID)'),
+        )
+        parser.add_argument(
+            '--description',
+            metavar='<description>',
+            help=_('Description of the limit'),
+        )
+        parser.add_argument(
+            '--resource-limit',
+            metavar='<resource-limit>',
+            dest='resource_limit',
+            type=int,
+            help=_('The resource limit for the project to assume'),
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        identity_client = self.app.client_manager.identity
+
+        limit = identity_client.limits.update(
+            parsed_args.limit_id,
+            description=parsed_args.description,
+            resource_limit=parsed_args.resource_limit
+        )
+
+        limit._info.pop('links', None)
+
+        return zip(*sorted(six.iteritems(limit._info)))
+
+
+class DeleteLimit(command.Command):
+    _description = _("Delete a limit")
+
+    def get_parser(self, prog_name):
+        parser = super(DeleteLimit, self).get_parser(prog_name)
+        parser.add_argument(
+            'limit_id',
+            metavar='<limit-id>',
+            nargs="+",
+            help=_('Limit to delete (ID)'),
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        identity_client = self.app.client_manager.identity
+
+        errors = 0
+        for limit_id in parsed_args.limit_id:
+            try:
+                identity_client.limits.delete(limit_id)
+            except Exception as e:
+                errors += 1
+                LOG.error(_("Failed to delete limit with ID "
+                            "'%(id)s': %(e)s"),
+                          {'id': limit_id, 'e': e})
+
+        if errors > 0:
+            total = len(parsed_args.limit_id)
+            msg = (_("%(errors)s of %(total)s limits failed to "
+                   "delete.") % {'errors': errors, 'total': total})
+            raise exceptions.CommandError(msg)
diff --git a/openstackclient/tests/functional/identity/v3/common.py b/openstackclient/tests/functional/identity/v3/common.py
index 525a31a218..58468bc7e7 100644
--- a/openstackclient/tests/functional/identity/v3/common.py
+++ b/openstackclient/tests/functional/identity/v3/common.py
@@ -59,6 +59,10 @@ class IdentityTests(base.TestCase):
     REGISTERED_LIMIT_LIST_HEADERS = ['ID', 'Service ID', 'Resource Name',
                                      'Default Limit', 'Description',
                                      'Region ID']
+    LIMIT_FIELDS = ['id', 'project_id', 'service_id', 'resource_name',
+                    'resource_limit', 'description', 'region_id']
+    LIMIT_LIST_HEADERS = ['ID', 'Project ID', 'Service ID', 'Resource Name',
+                          'Resource Limit', 'Description', 'Region ID']
 
     @classmethod
     def setUpClass(cls):
@@ -356,3 +360,42 @@ class IdentityTests(base.TestCase):
             for k, v in d.iteritems():
                 if k == key:
                     return v
+
+    def _create_dummy_limit(self, add_clean_up=True):
+        registered_limit_id = self._create_dummy_registered_limit()
+
+        raw_output = self.openstack(
+            'registered limit show %s' % registered_limit_id
+        )
+        items = self.parse_show(raw_output)
+        resource_name = self._extract_value_from_items('resource_name', items)
+        service_id = self._extract_value_from_items('service_id', items)
+        resource_limit = 15
+
+        project_name = self._create_dummy_project()
+        raw_output = self.openstack('project show %s' % project_name)
+        items = self.parse_show(raw_output)
+        project_id = self._extract_value_from_items('id', items)
+
+        params = {
+            'project_id': project_id,
+            'service_id': service_id,
+            'resource_name': resource_name,
+            'resource_limit': resource_limit
+        }
+
+        raw_output = self.openstack(
+            'limit create'
+            ' --project %(project_id)s'
+            ' --service %(service_id)s'
+            ' --resource-limit %(resource_limit)s'
+            ' %(resource_name)s' % params
+        )
+        items = self.parse_show(raw_output)
+        limit_id = self._extract_value_from_items('id', items)
+
+        if add_clean_up:
+            self.addCleanup(self.openstack, 'limit delete %s' % limit_id)
+
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
+        return limit_id
diff --git a/openstackclient/tests/functional/identity/v3/test_limit.py b/openstackclient/tests/functional/identity/v3/test_limit.py
new file mode 100644
index 0000000000..03bcb06e4b
--- /dev/null
+++ b/openstackclient/tests/functional/identity/v3/test_limit.py
@@ -0,0 +1,192 @@
+#    Licensed 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.
+
+from tempest.lib.common.utils import data_utils
+
+from openstackclient.tests.functional.identity.v3 import common
+
+
+class LimitTestCase(common.IdentityTests):
+
+    def test_limit_create_with_service_name(self):
+        registered_limit_id = self._create_dummy_registered_limit()
+        raw_output = self.openstack(
+            'registered limit show %s' % registered_limit_id
+        )
+        items = self.parse_show(raw_output)
+        service_id = self._extract_value_from_items('service_id', items)
+        resource_name = self._extract_value_from_items('resource_name', items)
+
+        raw_output = self.openstack('service show %s' % service_id)
+        items = self.parse_show(raw_output)
+        service_name = self._extract_value_from_items('name', items)
+
+        project_name = self._create_dummy_project()
+        raw_output = self.openstack('project show %s' % project_name)
+        items = self.parse_show(raw_output)
+        project_id = self._extract_value_from_items('id', items)
+
+        params = {
+            'project_id': project_id,
+            'service_name': service_name,
+            'resource_name': resource_name,
+            'resource_limit': 15
+        }
+        raw_output = self.openstack(
+            'limit create'
+            ' --project %(project_id)s'
+            ' --service %(service_name)s'
+            ' --resource-limit %(resource_limit)s'
+            ' %(resource_name)s' % params
+        )
+        items = self.parse_show(raw_output)
+        limit_id = self._extract_value_from_items('id', items)
+        self.addCleanup(self.openstack, 'limit delete %s' % limit_id)
+
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
+
+    def test_limit_create_with_project_name(self):
+        registered_limit_id = self._create_dummy_registered_limit()
+        raw_output = self.openstack(
+            'registered limit show %s' % registered_limit_id
+        )
+        items = self.parse_show(raw_output)
+        service_id = self._extract_value_from_items('service_id', items)
+        resource_name = self._extract_value_from_items('resource_name', items)
+
+        raw_output = self.openstack('service show %s' % service_id)
+        items = self.parse_show(raw_output)
+        service_name = self._extract_value_from_items('name', items)
+
+        project_name = self._create_dummy_project()
+
+        params = {
+            'project_name': project_name,
+            'service_name': service_name,
+            'resource_name': resource_name,
+            'resource_limit': 15
+        }
+        raw_output = self.openstack(
+            'limit create'
+            ' --project %(project_name)s'
+            ' --service %(service_name)s'
+            ' --resource-limit %(resource_limit)s'
+            ' %(resource_name)s' % params
+        )
+        items = self.parse_show(raw_output)
+        limit_id = self._extract_value_from_items('id', items)
+        self.addCleanup(self.openstack, 'limit delete %s' % limit_id)
+
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
+        registered_limit_id = self._create_dummy_registered_limit()
+
+    def test_limit_create_with_service_id(self):
+        self._create_dummy_limit()
+
+    def test_limit_create_with_project_id(self):
+        self._create_dummy_limit()
+
+    def test_limit_create_with_options(self):
+        registered_limit_id = self._create_dummy_registered_limit()
+        region_id = self._create_dummy_region()
+
+        params = {
+            'region_id': region_id,
+            'registered_limit_id': registered_limit_id
+        }
+
+        raw_output = self.openstack(
+            'registered limit set'
+            ' %(registered_limit_id)s'
+            ' --region %(region_id)s' % params
+        )
+        items = self.parse_show(raw_output)
+        service_id = self._extract_value_from_items('service_id', items)
+        resource_name = self._extract_value_from_items('resource_name', items)
+
+        project_name = self._create_dummy_project()
+        raw_output = self.openstack('project show %s' % project_name)
+        items = self.parse_show(raw_output)
+        project_id = self._extract_value_from_items('id', items)
+        description = data_utils.arbitrary_string()
+
+        params = {
+            'project_id': project_id,
+            'service_id': service_id,
+            'resource_name': resource_name,
+            'resource_limit': 15,
+            'region_id': region_id,
+            'description': description
+        }
+        raw_output = self.openstack(
+            'limit create'
+            ' --project %(project_id)s'
+            ' --service %(service_id)s'
+            ' --resource-limit %(resource_limit)s'
+            ' --region %(region_id)s'
+            ' --description %(description)s'
+            ' %(resource_name)s' % params
+        )
+        items = self.parse_show(raw_output)
+        limit_id = self._extract_value_from_items('id', items)
+        self.addCleanup(self.openstack, 'limit delete %s' % limit_id)
+
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
+
+    def test_limit_show(self):
+        limit_id = self._create_dummy_limit()
+        raw_output = self.openstack('limit show %s' % limit_id)
+        items = self.parse_show(raw_output)
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
+
+    def test_limit_set_description(self):
+        limit_id = self._create_dummy_limit()
+
+        params = {
+            'description': data_utils.arbitrary_string(),
+            'limit_id': limit_id
+        }
+
+        raw_output = self.openstack(
+            'limit set'
+            ' --description %(description)s'
+            ' %(limit_id)s' % params
+        )
+        items = self.parse_show(raw_output)
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
+
+    def test_limit_set_resource_limit(self):
+        limit_id = self._create_dummy_limit()
+
+        params = {
+            'resource_limit': 5,
+            'limit_id': limit_id
+        }
+
+        raw_output = self.openstack(
+            'limit set'
+            ' --resource-limit %(resource_limit)s'
+            ' %(limit_id)s' % params
+        )
+        items = self.parse_show(raw_output)
+        self.assert_show_fields(items, self.LIMIT_FIELDS)
+
+    def test_limit_list(self):
+        self._create_dummy_limit()
+        raw_output = self.openstack('limit list')
+        items = self.parse_listing(raw_output)
+        self.assert_table_structure(items, self.LIMIT_LIST_HEADERS)
+
+    def test_limit_delete(self):
+        limit_id = self._create_dummy_limit(add_clean_up=False)
+        raw_output = self.openstack('limit delete %s' % limit_id)
+        self.assertEqual(0, len(raw_output))
diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py
index 3cae45157d..27ee9fd026 100644
--- a/openstackclient/tests/unit/identity/v3/fakes.py
+++ b/openstackclient/tests/unit/identity/v3/fakes.py
@@ -507,6 +507,29 @@ REGISTERED_LIMIT_OPTIONS = {
     'region_id': region_id
 }
 
+limit_id = 'limit-id'
+limit_resource_limit = 15
+limit_description = 'limit of foobars'
+limit_resource_name = 'foobars'
+LIMIT = {
+    'id': limit_id,
+    'project_id': project_id,
+    'resource_limit': limit_resource_limit,
+    'resource_name': limit_resource_name,
+    'service_id': service_id,
+    'description': None,
+    'region_id': None
+}
+LIMIT_OPTIONS = {
+    'id': limit_id,
+    'project_id': project_id,
+    'resource_limit': limit_resource_limit,
+    'resource_name': limit_resource_name,
+    'service_id': service_id,
+    'description': limit_description,
+    'region_id': region_id
+}
+
 
 def fake_auth_ref(fake_token, fake_service=None):
     """Create an auth_ref using keystoneauth's fixtures"""
@@ -601,6 +624,8 @@ class FakeIdentityv3Client(object):
         self.inference_rules.resource_class = fakes.FakeResource(None, {})
         self.registered_limits = mock.Mock()
         self.registered_limits.resource_class = fakes.FakeResource(None, {})
+        self.limits = mock.Mock()
+        self.limits.resource_class = fakes.FakeResource(None, {})
 
 
 class FakeFederationManager(object):
diff --git a/openstackclient/tests/unit/identity/v3/test_limit.py b/openstackclient/tests/unit/identity/v3/test_limit.py
new file mode 100644
index 0000000000..44c0358d89
--- /dev/null
+++ b/openstackclient/tests/unit/identity/v3/test_limit.py
@@ -0,0 +1,382 @@
+#   Licensed 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.
+
+import copy
+
+from keystoneauth1.exceptions import http as ksa_exceptions
+from osc_lib import exceptions
+
+from openstackclient.identity.v3 import limit
+from openstackclient.tests.unit import fakes
+from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
+
+
+class TestLimit(identity_fakes.TestIdentityv3):
+
+    def setUp(self):
+        super(TestLimit, self).setUp()
+
+        identity_manager = self.app.client_manager.identity
+
+        self.limit_mock = identity_manager.limits
+
+        self.services_mock = identity_manager.services
+        self.services_mock.reset_mock()
+
+        self.projects_mock = identity_manager.projects
+        self.projects_mock.reset_mock()
+
+        self.regions_mock = identity_manager.regions
+        self.regions_mock.reset_mock()
+
+
+class TestLimitCreate(TestLimit):
+
+    def setUp(self):
+        super(TestLimitCreate, self).setUp()
+
+        self.service = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.SERVICE),
+            loaded=True
+        )
+        self.services_mock.get.return_value = self.service
+
+        self.project = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.PROJECT),
+            loaded=True
+        )
+        self.projects_mock.get.return_value = self.project
+
+        self.region = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.REGION),
+            loaded=True
+        )
+        self.regions_mock.get.return_value = self.region
+
+        self.cmd = limit.CreateLimit(self.app, None)
+
+    def test_limit_create_without_options(self):
+        self.limit_mock.create.return_value = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.LIMIT),
+            loaded=True
+        )
+
+        resource_limit = 15
+        arglist = [
+            '--project', identity_fakes.project_id,
+            '--service', identity_fakes.service_id,
+            '--resource-limit', str(resource_limit),
+            identity_fakes.limit_resource_name
+        ]
+        verifylist = [
+            ('project', identity_fakes.project_id),
+            ('service', identity_fakes.service_id),
+            ('resource_name', identity_fakes.limit_resource_name),
+            ('resource_limit', resource_limit)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        kwargs = {'description': None, 'region': None}
+        self.limit_mock.create.assert_called_with(
+            self.project,
+            self.service,
+            identity_fakes.limit_resource_name,
+            resource_limit,
+            **kwargs
+        )
+
+        collist = ('description', 'id', 'project_id', 'region_id',
+                   'resource_limit', 'resource_name', 'service_id')
+        self.assertEqual(collist, columns)
+        datalist = (
+            None,
+            identity_fakes.limit_id,
+            identity_fakes.project_id,
+            None,
+            resource_limit,
+            identity_fakes.limit_resource_name,
+            identity_fakes.service_id
+        )
+        self.assertEqual(datalist, data)
+
+    def test_limit_create_with_options(self):
+        self.limit_mock.create.return_value = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.LIMIT_OPTIONS),
+            loaded=True
+        )
+
+        resource_limit = 15
+        arglist = [
+            '--project', identity_fakes.project_id,
+            '--service', identity_fakes.service_id,
+            '--resource-limit', str(resource_limit),
+            '--region', identity_fakes.region_id,
+            '--description', identity_fakes.limit_description,
+            identity_fakes.limit_resource_name
+        ]
+        verifylist = [
+            ('project', identity_fakes.project_id),
+            ('service', identity_fakes.service_id),
+            ('resource_name', identity_fakes.limit_resource_name),
+            ('resource_limit', resource_limit),
+            ('region', identity_fakes.region_id),
+            ('description', identity_fakes.limit_description)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        kwargs = {
+            'description': identity_fakes.limit_description,
+            'region': self.region
+        }
+        self.limit_mock.create.assert_called_with(
+            self.project,
+            self.service,
+            identity_fakes.limit_resource_name,
+            resource_limit,
+            **kwargs
+        )
+
+        collist = ('description', 'id', 'project_id', 'region_id',
+                   'resource_limit', 'resource_name', 'service_id')
+        self.assertEqual(collist, columns)
+        datalist = (
+            identity_fakes.limit_description,
+            identity_fakes.limit_id,
+            identity_fakes.project_id,
+            identity_fakes.region_id,
+            resource_limit,
+            identity_fakes.limit_resource_name,
+            identity_fakes.service_id
+        )
+        self.assertEqual(datalist, data)
+
+
+class TestLimitDelete(TestLimit):
+
+    def setUp(self):
+        super(TestLimitDelete, self).setUp()
+        self.cmd = limit.DeleteLimit(self.app, None)
+
+    def test_limit_delete(self):
+        self.limit_mock.delete.return_value = None
+
+        arglist = [identity_fakes.limit_id]
+        verifylist = [
+            ('limit_id', [identity_fakes.limit_id])
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        result = self.cmd.take_action(parsed_args)
+
+        self.limit_mock.delete.assert_called_with(
+            identity_fakes.limit_id
+        )
+        self.assertIsNone(result)
+
+    def test_limit_delete_with_exception(self):
+        return_value = ksa_exceptions.NotFound()
+        self.limit_mock.delete.side_effect = return_value
+
+        arglist = ['fake-limit-id']
+        verifylist = [
+            ('limit_id', ['fake-limit-id'])
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        try:
+            self.cmd.take_action(parsed_args)
+            self.fail('CommandError should be raised.')
+        except exceptions.CommandError as e:
+            self.assertEqual(
+                '1 of 1 limits failed to delete.', str(e)
+            )
+
+
+class TestLimitShow(TestLimit):
+
+    def setUp(self):
+        super(TestLimitShow, self).setUp()
+
+        self.limit_mock.get.return_value = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.LIMIT),
+            loaded=True
+        )
+
+        self.cmd = limit.ShowLimit(self.app, None)
+
+    def test_limit_show(self):
+        arglist = [identity_fakes.limit_id]
+        verifylist = [('limit_id', identity_fakes.limit_id)]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self.limit_mock.get.assert_called_with(identity_fakes.limit_id)
+
+        collist = (
+            'description', 'id', 'project_id', 'region_id', 'resource_limit',
+            'resource_name', 'service_id'
+        )
+        self.assertEqual(collist, columns)
+        datalist = (
+            None,
+            identity_fakes.limit_id,
+            identity_fakes.project_id,
+            None,
+            identity_fakes.limit_resource_limit,
+            identity_fakes.limit_resource_name,
+            identity_fakes.service_id
+        )
+        self.assertEqual(datalist, data)
+
+
+class TestLimitSet(TestLimit):
+
+    def setUp(self):
+        super(TestLimitSet, self).setUp()
+        self.cmd = limit.SetLimit(self.app, None)
+
+    def test_limit_set_description(self):
+        limit = copy.deepcopy(identity_fakes.LIMIT)
+        limit['description'] = identity_fakes.limit_description
+        self.limit_mock.update.return_value = fakes.FakeResource(
+            None, limit, loaded=True
+        )
+
+        arglist = [
+            '--description', identity_fakes.limit_description,
+            identity_fakes.limit_id
+        ]
+        verifylist = [
+            ('description', identity_fakes.limit_description),
+            ('limit_id', identity_fakes.limit_id)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self.limit_mock.update.assert_called_with(
+            identity_fakes.limit_id,
+            description=identity_fakes.limit_description,
+            resource_limit=None
+        )
+
+        collist = (
+            'description', 'id', 'project_id', 'region_id', 'resource_limit',
+            'resource_name', 'service_id'
+        )
+        self.assertEqual(collist, columns)
+        datalist = (
+            identity_fakes.limit_description,
+            identity_fakes.limit_id,
+            identity_fakes.project_id,
+            None,
+            identity_fakes.limit_resource_limit,
+            identity_fakes.limit_resource_name,
+            identity_fakes.service_id
+        )
+        self.assertEqual(datalist, data)
+
+    def test_limit_set_resource_limit(self):
+        resource_limit = 20
+        limit = copy.deepcopy(identity_fakes.LIMIT)
+        limit['resource_limit'] = resource_limit
+        self.limit_mock.update.return_value = fakes.FakeResource(
+            None, limit, loaded=True
+        )
+
+        arglist = [
+            '--resource-limit', str(resource_limit),
+            identity_fakes.limit_id
+        ]
+        verifylist = [
+            ('resource_limit', resource_limit),
+            ('limit_id', identity_fakes.limit_id)
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self.limit_mock.update.assert_called_with(
+            identity_fakes.limit_id,
+            description=None,
+            resource_limit=resource_limit
+        )
+
+        collist = (
+            'description', 'id', 'project_id', 'region_id', 'resource_limit',
+            'resource_name', 'service_id'
+        )
+        self.assertEqual(collist, columns)
+        datalist = (
+            None,
+            identity_fakes.limit_id,
+            identity_fakes.project_id,
+            None,
+            resource_limit,
+            identity_fakes.limit_resource_name,
+            identity_fakes.service_id
+        )
+        self.assertEqual(datalist, data)
+
+
+class TestLimitList(TestLimit):
+
+    def setUp(self):
+        super(TestLimitList, self).setUp()
+
+        self.limit_mock.list.return_value = [
+            fakes.FakeResource(
+                None,
+                copy.deepcopy(identity_fakes.LIMIT),
+                loaded=True
+            )
+        ]
+
+        self.cmd = limit.ListLimit(self.app, None)
+
+    def test_limit_list(self):
+        arglist = []
+        verifylist = []
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        columns, data = self.cmd.take_action(parsed_args)
+
+        self.limit_mock.list.assert_called_with(
+            service=None, resource_name=None, region=None
+        )
+
+        collist = (
+            'ID', 'Project ID', 'Service ID', 'Resource Name',
+            'Resource Limit', 'Description', 'Region ID'
+        )
+        self.assertEqual(collist, columns)
+        datalist = ((
+            identity_fakes.limit_id,
+            identity_fakes.project_id,
+            identity_fakes.service_id,
+            identity_fakes.limit_resource_name,
+            identity_fakes.limit_resource_limit,
+            None,
+            None
+        ), )
+        self.assertEqual(datalist, tuple(data))
diff --git a/releasenotes/notes/bp-unified-limits-6c5fdb1c26805d86.yaml b/releasenotes/notes/bp-unified-limits-6c5fdb1c26805d86.yaml
new file mode 100644
index 0000000000..b00de40c69
--- /dev/null
+++ b/releasenotes/notes/bp-unified-limits-6c5fdb1c26805d86.yaml
@@ -0,0 +1,7 @@
+---
+features:
+  - |
+    [`bp unified-limits <https://blueprints.launchpad.net/keystone/+spec/unified-limit>`_]
+    Support has been added for managing project-specific limits in keystone via
+    the ``limit`` command. Limits define limits of resources for projects to
+    consume once a limit has been registered.
diff --git a/setup.cfg b/setup.cfg
index af7bbbf20e..64a5533eda 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -268,6 +268,12 @@ openstack.identity.v3 =
     implied_role_delete = openstackclient.identity.v3.implied_role:DeleteImpliedRole
     implied_role_list = openstackclient.identity.v3.implied_role:ListImpliedRole
 
+    limit_create = openstackclient.identity.v3.limit:CreateLimit
+    limit_delete = openstackclient.identity.v3.limit:DeleteLimit
+    limit_list = openstackclient.identity.v3.limit:ListLimit
+    limit_set = openstackclient.identity.v3.limit:SetLimit
+    limit_show = openstackclient.identity.v3.limit:ShowLimit
+
     mapping_create = openstackclient.identity.v3.mapping:CreateMapping
     mapping_delete = openstackclient.identity.v3.mapping:DeleteMapping
     mapping_list = openstackclient.identity.v3.mapping:ListMapping