diff --git a/synapse/config/_base.py b/synapse/config/_base.py
index 9b0f8c3c322d2b2bf9c8560f8ce21fd68bd309fb..87cdbf1d30dcbaf882726bef5a43e51a219a7d75 100644
--- a/synapse/config/_base.py
+++ b/synapse/config/_base.py
@@ -27,6 +27,16 @@ class Config(object):
     def __init__(self, args):
         pass
 
+    @staticmethod
+    def parse_size(string):
+        sizes = {"K": 1024, "M": 1024 * 1024}
+        size = 1
+        suffix = string[-1]
+        if suffix in sizes:
+            string = string[:-1]
+            size = sizes[suffix]
+        return int(string) * size
+
     @staticmethod
     def abspath(file_path):
         return os.path.abspath(file_path) if file_path else file_path
diff --git a/synapse/config/database.py b/synapse/config/database.py
index daa161c9520d2baf268159b099b4547d41d1b28e..87efe546452dd3e9e386f47dd23ad15da98aee50 100644
--- a/synapse/config/database.py
+++ b/synapse/config/database.py
@@ -24,6 +24,7 @@ class DatabaseConfig(Config):
             self.database_path = ":memory:"
         else:
             self.database_path = self.abspath(args.database_path)
+        self.event_cache_size = self.parse_size(args.event_cache_size)
 
     @classmethod
     def add_arguments(cls, parser):
@@ -33,6 +34,10 @@ class DatabaseConfig(Config):
             "-d", "--database-path", default="homeserver.db",
             help="The database name."
         )
+        db_group.add_argument(
+            "--event-cache-size", default="100K",
+            help="Number of events to cache in memory."
+        )
 
     @classmethod
     def generate_config(cls, args, config_dir_path):
diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py
index a63c59a8a25d8893809165f0f78e7318bbbac721..1170d8b6ec8da07fd99e7931e752fc055d9748ad 100644
--- a/synapse/storage/__init__.py
+++ b/synapse/storage/__init__.py
@@ -164,6 +164,9 @@ class DataStore(RoomMemberStore, RoomStore,
                            stream_ordering=None, is_new_state=True,
                            current_state=None):
 
+        # Remove the any existing cache entries for the event_id
+        self._get_event_cache.pop(event.event_id)
+
         # We purposefully do this first since if we include a `current_state`
         # key, we *want* to update the `current_state_events` table
         if current_state:
diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py
index 3e1ab0a159044bf9fe9dfbaaaef4762d802dc6e8..f13b8f4fad6d7c8ea2b5f2ca3ee1c3bd6b0d0eb1 100644
--- a/synapse/storage/_base.py
+++ b/synapse/storage/_base.py
@@ -19,6 +19,7 @@ from synapse.events import FrozenEvent
 from synapse.events.utils import prune_event
 from synapse.util.logutils import log_function
 from synapse.util.logcontext import PreserveLoggingContext, LoggingContext
+from synapse.util.lrucache import LruCache
 
 from twisted.internet import defer
 
@@ -128,6 +129,8 @@ class SQLBaseStore(object):
         self._txn_perf_counters = PerformanceCounters()
         self._get_event_counters = PerformanceCounters()
 
+        self._get_event_cache = LruCache(hs.config.event_cache_size)
+
     def start_profiling(self):
         self._previous_loop_ts = self._clock.time_msec()
 
@@ -579,6 +582,20 @@ class SQLBaseStore(object):
 
     def _get_event_txn(self, txn, event_id, check_redacted=True,
                        get_prev_content=False, allow_rejected=False):
+
+        start_time = time.time() * 1000
+        update_counter = self._get_event_counters.update
+
+        try:
+            cache = self._get_event_cache.setdefault(event_id, {})
+            # Separate cache entries for each way to invoke _get_event_txn
+            return cache[(check_redacted, get_prev_content, allow_rejected)]
+        except KeyError:
+            pass
+        finally:
+            start_time = update_counter("event_cache", start_time)
+
+
         sql = (
             "SELECT e.internal_metadata, e.json, r.event_id, rej.reason "
             "FROM event_json as e "
@@ -588,7 +605,6 @@ class SQLBaseStore(object):
             "LIMIT 1 "
         )
 
-        start_time = time.time() * 1000
 
         txn.execute(sql, (event_id,))
 
@@ -599,14 +615,16 @@ class SQLBaseStore(object):
 
         internal_metadata, js, redacted, rejected_reason = res
 
-        self._get_event_counters.update("select_event", start_time)
+        start_time = update_counter("select_event", start_time)
 
         if allow_rejected or not rejected_reason:
-            return self._get_event_from_row_txn(
+            result = self._get_event_from_row_txn(
                 txn, internal_metadata, js, redacted,
                 check_redacted=check_redacted,
                 get_prev_content=get_prev_content,
             )
+            cache[(check_redacted, get_prev_content, allow_rejected)] = result
+            return result
         else:
             return None
 
diff --git a/tests/storage/test_base.py b/tests/storage/test_base.py
index a0a24ce096948cf71eb8d88ee1c94ba4ae18b85e..55fbffa7a202330798670ae9b42587187322aff0 100644
--- a/tests/storage/test_base.py
+++ b/tests/storage/test_base.py
@@ -38,8 +38,9 @@ class SQLBaseStoreTestCase(unittest.TestCase):
             return defer.succeed(func(self.mock_txn, *args, **kwargs))
         self.db_pool.runInteraction = runInteraction
 
-        hs = HomeServer("test",
-                db_pool=self.db_pool)
+        config = Mock()
+        config.event_cache_size = 1
+        hs = HomeServer("test", db_pool=self.db_pool, config=config)
 
         self.datastore = SQLBaseStore(hs)
 
diff --git a/tests/utils.py b/tests/utils.py
index 25c33492a50d305444f25f11548615d6b25cfea8..39895c739f7833dcf02adf12e0939f4486576edf 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -41,6 +41,7 @@ def setup_test_homeserver(name="test", datastore=None, config=None, **kargs):
     if config is None:
         config = Mock()
         config.signing_key = [MockKey()]
+        config.event_cache_size = 1
 
     if datastore is None:
         db_pool = SQLiteMemoryDbPool()