diff --git a/CHANGES.rst b/CHANGES.rst
index 3decb56..b6c3610 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,15 +4,15 @@ Latest changes
 In development
 --------------
 
-- Ensure native byte order for memmap arrays in `joblib.load`.
+- Ensure native byte order for memmap arrays in ``joblib.load``.
   https://github.com/joblib/joblib/issues/1353
 
 - Add ability to change default Parallel backend in tests by setting the
-  JOBLIB_TESTS_DEFAULT_PARALLEL_BACKEND environment variable.
+  ``JOBLIB_TESTS_DEFAULT_PARALLEL_BACKEND`` environment variable.
   https://github.com/joblib/joblib/pull/1356
 
 - Fix temporary folder creation in `joblib.Parallel` on Linux subsystems on Windows
-  which do have `/dev/shm` but don't have the `os.statvfs` function 
+  which do have `/dev/shm` but don't have the `os.statvfs` function
   https://github.com/joblib/joblib/issues/1353
 
 - Drop runtime dependency on ``distutils``. ``distutils`` is going away
@@ -24,7 +24,7 @@ In development
 
 - A warning is raised when a pickling error occurs during caching operations.
   In version 1.5, this warning will be turned into an error. For all other
-  errors, a new warning has been introduced: `joblib.memory.CacheWarning`.
+  errors, a new warning has been introduced: ``joblib.memory.CacheWarning``.
   https://github.com/joblib/joblib/pull/1359
 
 - Avoid (module, name) collisions when caching nested functions. This fix
@@ -40,12 +40,18 @@ In development
   tracebacks and more efficient running time.
   https://github.com/joblib/joblib/pull/1393
 
-- Add the `parallel_config` context manager to allow for more fine-grained
+- Add the ``parallel_config`` context manager to allow for more fine-grained
   control over the backend configuration. It should be used in place of the
-  `parallel_backend` context manager. In particular, it has the advantage
+  ``parallel_backend`` context manager. In particular, it has the advantage
   of not requiring to set a specific backend in the context manager.
   https://github.com/joblib/joblib/pull/1392
 
+- Add ``items_limit`` and ``age_limit`` in :meth:`joblib.Memory.reduce_size`
+  to make it easy to limit the number of items and remove items that have
+  not been accessed for a long time in the cache.
+  https://github.com/joblib/joblib/pull/1200
+
+
 Release 1.2.0
 -------------
 
@@ -59,7 +65,7 @@ Release 1.2.0
 
 - Avoid unnecessary warnings when workers and main process delete
   the temporary memmap folder contents concurrently.
-  https://github.com/joblib/joblib/pull/1263 
+  https://github.com/joblib/joblib/pull/1263
 
 - Fix memory alignment bug for pickles containing numpy arrays.
   This is especially important when loading the pickle with
@@ -118,7 +124,7 @@ Release 1.0.1
 
 - Add check_call_in_cache method to check cache without calling function.
   https://github.com/joblib/joblib/pull/820
- 
+
 - dask: avoid redundant scattering of large arguments to make a more
   efficient use of the network resources and avoid crashing dask with
   "OSError: [Errno 55] No buffer space available"
@@ -134,7 +140,7 @@ Release 1.0.0
   or a third party library involved in the cached values definition is
   upgraded.  In particular, users updating `joblib` to a release that includes
   this fix will see their previous cache invalidated if they contained
-  reference to `numpy` objects. 
+  reference to `numpy` objects.
   https://github.com/joblib/joblib/pull/1136
 
 - Remove deprecated `check_pickle` argument in `delayed`.
diff --git a/joblib/_store_backends.py b/joblib/_store_backends.py
index 21a4958..de1e939 100644
--- a/joblib/_store_backends.py
+++ b/joblib/_store_backends.py
@@ -294,9 +294,15 @@ class StoreBackendMixin(object):
         """Clear the whole store content."""
         self.clear_location(self.location)
 
-    def reduce_store_size(self, bytes_limit):
-        """Reduce store size to keep it under the given bytes limit."""
-        items_to_delete = self._get_items_to_delete(bytes_limit)
+    def enforce_store_limits(
+            self, bytes_limit, items_limit=None, age_limit=None
+    ):
+        """
+        Remove the store's oldest files to enforce item, byte, and age limits.
+        """
+        items_to_delete = self._get_items_to_delete(
+            bytes_limit, items_limit, age_limit
+        )
 
         for item in items_to_delete:
             if self.verbose > 10:
@@ -310,16 +316,38 @@ class StoreBackendMixin(object):
                 # the folder already.
                 pass
 
-    def _get_items_to_delete(self, bytes_limit):
-        """Get items to delete to keep the store under a size limit."""
+    def _get_items_to_delete(
+            self, bytes_limit, items_limit=None, age_limit=None
+    ):
+        """
+        Get items to delete to keep the store under size, file, & age limits.
+        """
         if isinstance(bytes_limit, str):
             bytes_limit = memstr_to_bytes(bytes_limit)
 
         items = self.get_items()
         size = sum(item.size for item in items)
 
-        to_delete_size = size - bytes_limit
-        if to_delete_size < 0:
+        if bytes_limit is not None:
+            to_delete_size = size - bytes_limit
+        else:
+            to_delete_size = 0
+
+        if items_limit is not None:
+            to_delete_items = len(items) - items_limit
+        else:
+            to_delete_items = 0
+
+        if age_limit is not None:
+            older_item = min(item.last_access for item in items)
+            deadline = datetime.datetime.now() - age_limit
+        else:
+            deadline = None
+
+        if (
+            to_delete_size <= 0 and to_delete_items <= 0
+            and (deadline is None or older_item > deadline)
+        ):
             return []
 
         # We want to delete first the cache items that were accessed a
@@ -328,13 +356,19 @@ class StoreBackendMixin(object):
 
         items_to_delete = []
         size_so_far = 0
+        items_so_far = 0
 
         for item in items:
-            if size_so_far > to_delete_size:
+            if (
+                (size_so_far >= to_delete_size)
+                and items_so_far >= to_delete_items
+                and (deadline is None or deadline < item.last_access)
+            ):
                 break
 
             items_to_delete.append(item)
             size_so_far += item.size
+            items_so_far += 1
 
         return items_to_delete
 
diff --git a/joblib/memory.py b/joblib/memory.py
index 90fe41e..70d0f60 100644
--- a/joblib/memory.py
+++ b/joblib/memory.py
@@ -926,10 +926,12 @@ class Memory(Logger):
             Verbosity flag, controls the debug messages that are issued
             as functions are evaluated.
 
-        bytes_limit: int, optional
+        bytes_limit: int | str, optional
             Limit in bytes of the size of the cache. By default, the size of
             the cache is unlimited. When reducing the size of the cache,
-            ``joblib`` keeps the most recently accessed items first.
+            ``joblib`` keeps the most recently accessed items first. If a
+            str is passed, it is converted to a number of bytes using units
+            { K | M | G} for kilo, mega, giga.
 
             **Note:** You need to call :meth:`joblib.Memory.reduce_size` to
             actually reduce the cache size to be less than ``bytes_limit``.
@@ -1022,16 +1024,53 @@ class Memory(Logger):
         if self.store_backend is not None:
             self.store_backend.clear()
 
-            # As the cache in completely clear, make sure the _FUNCTION_HASHES
+            # As the cache is completely clear, make sure the _FUNCTION_HASHES
             # cache is also reset. Else, for a function that is present in this
             # table, results cached after this clear will be have cache miss
             # as the function code is not re-written.
             _FUNCTION_HASHES.clear()
 
-    def reduce_size(self):
-        """Remove cache elements to make cache size fit in ``bytes_limit``."""
-        if self.bytes_limit is not None and self.store_backend is not None:
-            self.store_backend.reduce_store_size(self.bytes_limit)
+    def reduce_size(self, bytes_limit=None, items_limit=None, age_limit=None):
+        """Remove cache elements to make the cache fit its limits.
+
+        The limitation can impose that the cache size fits in ``bytes_limit``,
+        that the number of cache items is no more than ``items_limit``, and
+        that all files in cache are not older than ``age_limit``.
+
+        Parameters
+        ----------
+        bytes_limit: int | str, optional
+            Limit in bytes of the size of the cache. By default, the size of
+            the cache is unlimited. When reducing the size of the cache,
+            ``joblib`` keeps the most recently accessed items first. If a
+            str is passed, it is converted to a number of bytes using units
+            { K | M | G} for kilo, mega, giga.
+
+        items_limit: int, optional
+            Number of items to limit the cache to.  By default, the number of
+            items in the cache is unlimited.  When reducing the size of the
+            cache, ``joblib`` keeps the most recently accessed items first.
+
+        age_limit: datetime.timedelta, optional
+            Maximum age of items to limit the cache to.  When reducing the size
+            of the cache, any items last accessed more than the given length of
+            time ago are deleted.
+        """
+        if bytes_limit is None:
+            bytes_limit = self.bytes_limit
+
+        if self.store_backend is None:
+            # No cached results, this function does nothing.
+            return
+
+        if bytes_limit is None and items_limit is None and age_limit is None:
+            # No limitation to impose, returning
+            return
+
+        # Defers the actual limits enforcing to the store backend.
+        self.store_backend.enforce_store_limits(
+            bytes_limit, items_limit, age_limit
+        )
 
     def eval(self, func, *args, **kwargs):
         """ Eval function func with arguments `*args` and `**kwargs`,

diff --git a/joblib/test/test_memory.py b/joblib/test/test_memory.py
index 035e9a1..5fa9d53 100644
--- a/joblib/test/test_memory.py
+++ b/joblib/test/test_memory.py
@@ -942,7 +942,7 @@ def test__get_items_to_delete(tmpdir):
             min(ci.last_access for ci in surviving_items))
 
 
-def test_memory_reduce_size(tmpdir):
+def test_memory_reduce_size_bytes_limit(tmpdir):
     memory, _, _ = _setup_toy_cache(tmpdir)
     ref_cache_items = memory.store_backend.get_items()
 
@@ -973,6 +973,64 @@ def test_memory_reduce_size(tmpdir):
     assert cache_items == []
 
 
+def test_memory_reduce_size_items_limit(tmpdir):
+    memory, _, _ = _setup_toy_cache(tmpdir)
+    ref_cache_items = memory.store_backend.get_items()
+
+    # By default reduce_size is a noop
+    memory.reduce_size()
+    cache_items = memory.store_backend.get_items()
+    assert sorted(ref_cache_items) == sorted(cache_items)
+
+    # No cache items deleted if items_limit greater than the size of
+    # the cache
+    memory.reduce_size(items_limit=10)
+    cache_items = memory.store_backend.get_items()
+    assert sorted(ref_cache_items) == sorted(cache_items)
+
+    # items_limit is set so that only two cache items are kept
+    memory.reduce_size(items_limit=2)
+    cache_items = memory.store_backend.get_items()
+    assert set.issubset(set(cache_items), set(ref_cache_items))
+    assert len(cache_items) == 2
+
+    # bytes_limit set so that no cache item is kept
+    memory.reduce_size(items_limit=0)
+    cache_items = memory.store_backend.get_items()
+    assert cache_items == []
+
+
+def test_memory_reduce_size_age_limit(tmpdir):
+    import time
+    import datetime
+    memory, _, put_cache = _setup_toy_cache(tmpdir)
+    ref_cache_items = memory.store_backend.get_items()
+
+    # By default reduce_size is a noop
+    memory.reduce_size()
+    cache_items = memory.store_backend.get_items()
+    assert sorted(ref_cache_items) == sorted(cache_items)
+
+    # No cache items deleted if age_limit big.
+    memory.reduce_size(age_limit=datetime.timedelta(days=1))
+    cache_items = memory.store_backend.get_items()
+    assert sorted(ref_cache_items) == sorted(cache_items)
+
+    # age_limit is set so that only two cache items are kept
+    time.sleep(1)
+    put_cache(-1)
+    put_cache(-2)
+    memory.reduce_size(age_limit=datetime.timedelta(seconds=1))
+    cache_items = memory.store_backend.get_items()
+    assert not set.issubset(set(cache_items), set(ref_cache_items))
+    assert len(cache_items) == 2
+
+    # age_limit set so that no cache item is kept
+    memory.reduce_size(age_limit=datetime.timedelta(seconds=0))
+    cache_items = memory.store_backend.get_items()
+    assert cache_items == []
+
+
 def test_memory_clear(tmpdir):
     memory, _, g = _setup_toy_cache(tmpdir)
     memory.clear()

Wishlist: more flexible control (not just by size) for clean up
Follow up to #1170 . Thank you for the clarifications on logic -- great to know that both `last_access` attribute it stored!

In our use case (https://github.com/con/fscacher/) size though is not an issue -- it is just a sheer number of files, and thus inodes consumed. I made a mistake to use "good old" EXT4 for HOME partition instead of BTRFS as for the main bulk storage and now I am paying the price of "running out of space" while still having GBs of actual space available. There is virtually no rule of thumb for what size (in bytes) we should aim for in our use case.  But it might be feasible to figure out how many cached items to keep (also the information which is available since you know the number of cached items) and/or even better -- for how long (known since `last_access` is stored). I would have then set in our case cache to not exceed 10k entries and be no older than 1 week or so.

Do you think such modes of cleanup could be added to `reduce_size` call?  (due to 
(FWIW: unless `reduce_size` is to be triggered automagically, I see no reason for `bytes_limit` or any of those settings actually to be listed in the constructor, and not just to become arguments for `reduce_size` call).
joblib/test/test_memory.py::test_memory_reduce_size_items_limit joblib/test/test_memory.py::test_memory_reduce_size_age_limit
joblib/test/test_memory.py::test_memory_integration joblib/test/test_memory.py::test_no_memory joblib/test/test_memory.py::test_memory_kwarg joblib/test/test_memory.py::test_memory_lambda joblib/test/test_memory.py::test_memory_name_collision joblib/test/test_memory.py::test_memory_warning_lambda_collisions joblib/test/test_memory.py::test_memory_warning_collision_detection joblib/test/test_memory.py::test_memory_partial joblib/test/test_memory.py::test_memory_eval joblib/test/test_memory.py::test_argument_change joblib/test/test_memory.py::test_memory_exception joblib/test/test_memory.py::test_memory_ignore joblib/test/test_memory.py::test_memory_ignore_decorated joblib/test/test_memory.py::test_memory_args_as_kwargs joblib/test/test_memory.py::test_partial_decoration[ignore0-100-r] joblib/test/test_memory.py::test_partial_decoration[ignore1-10-None] joblib/test/test_memory.py::test_func_dir joblib/test/test_memory.py::test_persistence joblib/test/test_memory.py::test_check_call_in_cache joblib/test/test_memory.py::test_call_and_shelve joblib/test/test_memory.py::test_call_and_shelve_argument_hash joblib/test/test_memory.py::test_call_and_shelve_lazily_load_stored_result joblib/test/test_memory.py::test_memorized_pickling joblib/test/test_memory.py::test_memorized_repr joblib/test/test_memory.py::test_memory_file_modification joblib/test/test_memory.py::test_memory_in_memory_function_code_change joblib/test/test_memory.py::test_clear_memory_with_none_location joblib/test/test_memory.py::test_memory_func_with_kwonly_args joblib/test/test_memory.py::test_memory_func_with_signature joblib/test/test_memory.py::test__get_items joblib/test/test_memory.py::test__get_items_to_delete joblib/test/test_memory.py::test_memory_reduce_size_bytes_limit joblib/test/test_memory.py::test_memory_clear joblib/test/test_memory.py::test_cached_function_race_condition_when_persisting_output joblib/test/test_memory.py::test_cached_function_race_condition_when_persisting_output_2 joblib/test/test_memory.py::test_memory_recomputes_after_an_error_while_loading_results joblib/test/test_memory.py::test_register_invalid_store_backends_key[None] joblib/test/test_memory.py::test_register_invalid_store_backends_key[invalid_prefix1] joblib/test/test_memory.py::test_register_invalid_store_backends_key[invalid_prefix2] joblib/test/test_memory.py::test_register_invalid_store_backends_object joblib/test/test_memory.py::test_memory_default_store_backend joblib/test/test_memory.py::test_warning_on_unknown_location_type joblib/test/test_memory.py::test_instanciate_incomplete_store_backend joblib/test/test_memory.py::test_dummy_store_backend joblib/test/test_memory.py::test_instanciate_store_backend_with_pathlib_path joblib/test/test_memory.py::test_filesystem_store_backend_repr joblib/test/test_memory.py::test_memory_objects_repr joblib/test/test_memory.py::test_memorized_result_pickle joblib/test/test_memory.py::test_memory_pickle_dump_load[memory_kwargs0] joblib/test/test_memory.py::test_memory_pickle_dump_load[memory_kwargs1] joblib/test/test_memory.py::test_info_log