diff --git a/reviewboard/admin/read_only.py b/reviewboard/admin/read_only.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c6bb23025253252b7499b19dd7ffcc1be057171
--- /dev/null
+++ b/reviewboard/admin/read_only.py
@@ -0,0 +1,30 @@
+"""Provide utility methods for read-only mode."""
+
+from __future__ import unicode_literals
+
+from djblets.siteconfig.models import SiteConfiguration
+
+
+def is_site_read_only_for(user):
+    """Check whether user should be affected by read-only mode.
+
+    Superusers are not affected by read-only mode. Otherwise, check the
+    current :py:class:`~djblets.siteconfig.models.SiteConfiguration`" for
+    ``site_read_only``.
+
+    Args:
+        user (django.contrib.auth.models.User):
+            The user that is to be checked.
+
+    Returns:
+        bool:
+        A boolean representing whether the site is read-only for a user.
+    """
+    if user.is_superuser:
+        return False
+
+    if not hasattr(user, '_cache_site_is_read_only'):
+        siteconfig = SiteConfiguration.objects.get_current()
+        user._cache_site_is_read_only = siteconfig.get('site_read_only')
+
+    return user._cache_site_is_read_only
diff --git a/reviewboard/webapi/base.py b/reviewboard/webapi/base.py
index 3c7e8e76dfcd64036f115f5c3ba535ffe87dc667..b344d41e17169beccf0a983d695f5b8bda9a1032 100644
--- a/reviewboard/webapi/base.py
+++ b/reviewboard/webapi/base.py
@@ -17,11 +17,13 @@ from djblets.webapi.resources.base import \
 from djblets.webapi.resources.mixins.api_tokens import ResourceAPITokenMixin
 from djblets.webapi.resources.mixins.queries import APIQueryUtilsMixin
 
+from reviewboard.admin.read_only import is_site_read_only_for
 from reviewboard.registries.registry import Registry
 from reviewboard.site.models import LocalSite
 from reviewboard.site.urlresolvers import local_site_reverse
 from reviewboard.webapi.decorators import (webapi_check_local_site,
                                            webapi_check_login_required)
+from reviewboard.webapi.errors import READ_ONLY_ERROR
 from reviewboard.webapi.models import WebAPIToken
 
 
@@ -204,12 +206,17 @@ class WebAPIResource(ResourceAPITokenMixin, APIQueryUtilsMixin,
         If a feature specified in that list is disabled, this method will
         return a 403 Forbidden response instead of calling the method view.
 
-        In addition, Review Board has token access policies. If the client is
-        authenticated with an API token, the token's access policies will be
-        checked before calling the view. If the operation is disallowed, a 403
-        Forbidden response will be returned.
+        Review Board has token access policies. If the client is authenticated
+        with an API token, the token's access policies will be checked before
+        calling the view. If the operation is disallowed, a 403 Forbidden
+        response will be returned.
 
-        Only if those two conditions are met will the view actually be called.
+        If read-only mode is enabled, all PUT, POST, and DELETE requests will
+        be rejected with a 503 Service Unavailable unless the user is a
+        superuser.
+
+        Only if these three conditions are met will the view actually be
+        called.
 
         Args:
             request (django.http.HttpRequest):
@@ -239,6 +246,10 @@ class WebAPIResource(ResourceAPITokenMixin, APIQueryUtilsMixin,
             if not feature.is_enabled(request=request):
                 return PERMISSION_DENIED
 
+        if (is_site_read_only_for(request.user) and
+            request.method not in ('GET', 'HEAD', 'OPTIONS')):
+            return READ_ONLY_ERROR
+
         return super(WebAPIResource, self).call_method_view(
             request, method, view, *args, **kwargs)
 
diff --git a/reviewboard/webapi/errors.py b/reviewboard/webapi/errors.py
index 3f9bb9df05ad064d0c5efdddd20e74b2cce07273..0afeeb6e56a07632272979b20c87245002d44731 100644
--- a/reviewboard/webapi/errors.py
+++ b/reviewboard/webapi/errors.py
@@ -166,3 +166,8 @@ REOPEN_ERROR = WebAPIError(
     231,
     'An error occurred while reopening the review request.',
     http_status=500)  # 500 Internal Server Error
+
+READ_ONLY_ERROR = WebAPIError(
+    232,
+    'The site is currently in read-only mode.',
+    http_status=503)  # 503 Service Unavailable Error
diff --git a/reviewboard/webapi/tests/test_base.py b/reviewboard/webapi/tests/test_base.py
index 4542673a867ad28c49ee5de2e6e076dceab3a6d4..97f8a2e633aafa035fb3323720e7a6173645684c 100644
--- a/reviewboard/webapi/tests/test_base.py
+++ b/reviewboard/webapi/tests/test_base.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
 
 import json
 
+from django.contrib.auth.models import AnonymousUser, User
 from django.test.client import RequestFactory
 from djblets.features import Feature, get_features_registry
 from djblets.testing.decorators import add_fixtures
@@ -11,6 +12,7 @@ from djblets.webapi.errors import PERMISSION_DENIED
 
 from reviewboard.site.models import LocalSite
 from reviewboard.webapi.base import WebAPIResource
+from reviewboard.webapi.errors import READ_ONLY_ERROR
 from reviewboard.webapi.tests.base import BaseWebAPITestCase
 
 
@@ -56,6 +58,151 @@ class BaseDummyResource(WebAPIResource):
         return 418, {'dummy': dummy}
 
 
+class WebAPIResourceReadOnlyTests(BaseWebAPITestCase):
+    """Tests for Web API Resources under read-only mode"""
+
+    def setUp(self):
+        super(WebAPIResourceReadOnlyTests, self).setUp()
+
+        self.resource = BaseDummyResource()
+
+    def tearDown(self):
+        super(WebAPIResourceReadOnlyTests, self).tearDown()
+
+        defaults = self.siteconfig.get_defaults()
+
+        self.siteconfig.set('site_read_only', defaults.get('site_read_only'))
+        self.siteconfig.save()
+
+    def test_read_only_update(self):
+        """Testing PUT with read only mode enabled returns
+        READ_ONLY_ERROR
+        """
+        self._test_method('put', read_only_enabled=True, is_superuser=False,
+                          expect_503=True, dummy=123)
+
+    def test_read_only_create(self):
+        """Testing POST with read only mode enabled returns
+        READ_ONLY_ERROR
+        """
+        self._test_method('post', read_only_enabled=True, is_superuser=False,
+                          expect_503=True, )
+
+    def test_read_only_delete(self):
+        """Testing PUT with read only mode enabled returns
+        READ_ONLY_ERROR
+        """
+        self._test_method('delete', read_only_enabled=True,
+                          is_superuser=False, expect_503=True)
+
+    def test_read_only_get(self):
+        """Testing GET with read only mode enabled returns a valid
+        response
+        """
+        self._test_method('get', read_only_enabled=True, is_superuser=False,
+                          expect_503=False)
+
+    def test_no_read_only_update(self):
+        """Testing PUT with read only mode disabled returns a valid
+        response
+        """
+        self._test_method('put', read_only_enabled=False, is_superuser=False,
+                          expect_503=False, dummy=123)
+
+    def test_no_read_only_create(self):
+        """Testing POST with read only mode disabled returns a valid
+        response
+        """
+        self._test_method('post', read_only_enabled=False, is_superuser=False,
+                          expect_503=False)
+
+    def test_no_read_only_delete(self):
+        """Testing PUT with read only mode disabled returns a valid
+        response
+        """
+        self._test_method('delete', read_only_enabled=False,
+                          is_superuser=False, expect_503=False)
+
+    def test_no_read_only_get(self):
+        """Testing GET with read only mode disabled returns a valid
+        response
+        """
+        self._test_method('get', read_only_enabled=False, is_superuser=False,
+                          expect_503=False)
+
+    def test_read_only_superuser_update(self):
+        """Testing PUT with read only mode enabled for superusers
+        returns a valid response
+        """
+        self._test_method('put', read_only_enabled=True, is_superuser=True,
+                          expect_503=False, dummy=123)
+
+    def test_read_only_superuser_create(self):
+        """Testing POST with read only mode enabled for superusers
+        returns a valid response
+        """
+        self._test_method('post', read_only_enabled=True, is_superuser=True,
+                          expect_503=False)
+
+    def test_read_only_superuser_delete(self):
+        """Testing DELETE with read only mode enabled for superusers
+        returns a valid response
+        """
+        self._test_method('delete', read_only_enabled=True, is_superuser=True,
+                          expect_503=False)
+
+    def test_read_only_superuser_get(self):
+        """Testing GET with read only mode enabled for superusers
+        returns a valid response
+        """
+        self._test_method('get', read_only_enabled=True, is_superuser=True,
+                          expect_503=False)
+
+    def _test_method(self, method, read_only_enabled, is_superuser,
+                     expect_503, dummy=None):
+        """Test request with given HTTP method for users and superusers
+        with read-only mode enabled or disabled and check that the results
+        are equal to the expected values.
+
+        Args:
+            method (:py:class:`unicode`):
+                String representing the HTTP method to test.
+
+            read_only_enabled (:py:class:`bool`):
+                Whether to test with read-only mode enabled.
+
+            is_superuser (:py:class:`bool`):
+                Whether to test with a user or superuser.
+
+            expect_503 (:py:class:`bool`):
+                Whether to expect a 503 or not as the response code.
+
+            dummy (int):
+                Content to pass to request to check it is returned in
+                response.
+        """
+        self.siteconfig.set('site_read_only', read_only_enabled)
+        self.siteconfig.save()
+
+        request = getattr(RequestFactory(), method)('/')
+        request.session = {}
+        request.user = User(username='dummy', is_superuser=is_superuser)
+
+        rsp = self.resource(request, dummy=dummy)
+
+        content = json.loads(rsp.content)
+
+        if expect_503:
+            self.assertEqual(rsp.status_code, 503)
+            self.assertEqual(content['stat'], 'fail')
+            self.assertEqual(content['err']['msg'], READ_ONLY_ERROR.msg)
+            self.assertEqual(content['err']['code'], READ_ONLY_ERROR.code)
+        else:
+            self.assertEqual(rsp.status_code, 418)
+            self.assertEqual(content['stat'], 'ok')
+            self.assertEqual(content['dummy'], dummy)
+
+
 class WebAPIResourceFeatureTests(BaseWebAPITestCase):
     """Tests for Web API Resources with required features"""
 
@@ -253,6 +400,7 @@ class WebAPIResourceFeatureTests(BaseWebAPITestCase):
         request = getattr(RequestFactory(), method)('/')
         request.local_site = local_site
         request.session = {}
+        request.user = AnonymousUser()
 
         with self.settings(**settings):
             rsp = self.resource(request, dummy=dummy)
