diff --git a/README.md b/README.md
index 66cb2560173481a7d50df800aeebebeab9864a94..9133982334efc6ec02515bbca95f07841b65b035 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,7 @@ Then copy your resulting wheel and install it via pip on the target system.
 ## v0.4.5
 
 * if page_size is default, it won't be written as part of the metadata
+* added support for per-series metadata
 
 ## v0.4.4
 
diff --git a/setup.py b/setup.py
index dc3677ac408d44b19a95918e6264afb90ecb85be..73b48e04f19a2131ac8df7b4320751f8afac662f 100644
--- a/setup.py
+++ b/setup.py
@@ -28,7 +28,7 @@ if 'CI' in os.environ:
 
 
 setup(name='tempsdb',
-      version='0.4.5a2',
+      version='0.4.5a3',
       packages=['tempsdb'],
       install_requires=['satella>=2.14.24', 'ujson'],
       ext_modules=build([Multibuild('tempsdb', find_pyx('tempsdb')), ],
diff --git a/tempsdb/series.pxd b/tempsdb/series.pxd
index 15335fd3f0282647d763ff52fba385217c947827..7f12446814347b783d4a04f2e09ffdf719740363 100644
--- a/tempsdb/series.pxd
+++ b/tempsdb/series.pxd
@@ -14,6 +14,7 @@ cdef class TimeSeries:
         readonly unsigned int block_size
         readonly unsigned long long last_entry_ts
         unsigned int page_size
+        readonly dict metadata
         readonly bint descriptor_based_access
         list chunks
         dict refs_chunks        # type: tp.Dict[int, int]
@@ -40,6 +41,7 @@ cdef class TimeSeries:
     cpdef tuple get_current_value(self)
     cpdef int disable_mmap(self) except -1
     cpdef int enable_mmap(self) except -1
+    cpdef int set_metadata(self, dict new_meta) except -1
     cdef inline int get_references_for(self, unsigned long long timestamp):
         return self.refs_chunks.get(timestamp, 0)
 
diff --git a/tempsdb/series.pyx b/tempsdb/series.pyx
index a3fdbd48e298541673ce81a34383f3c868af7a19..98bc8a99411534c61493a787d681be6cb7ff9c53 100644
--- a/tempsdb/series.pyx
+++ b/tempsdb/series.pyx
@@ -1,3 +1,4 @@
+import typing as tp
 import shutil
 import threading
 from satella.json import write_json_to_file, read_json_from_file
@@ -20,6 +21,7 @@ cdef class TimeSeries:
     :ivar path: path to the directory containing the series (str)
     :ivar descriptor_based_access: are all chunks using descriptor-based access? (bool)
     :ivar name: name of the series (str)
+    :ivar metadata: extra data (tp.Optional[dict])
     """
     cpdef tuple get_current_value(self):
         """
@@ -51,6 +53,18 @@ cdef class TimeSeries:
                 chunk.switch_to_descriptor_based_access()
         return 0
 
+    cpdef int set_metadata(self, dict new_meta) except -1:
+        """
+        Set a new value for the :attr:`~tempsdb.series.TimeSeries.metadata` property.
+        
+        This writes the disk.
+        
+        :param new_meta: new value of metadata property
+        """
+        self.metadata = new_meta
+        self.sync_metadata()
+        return 0
+
     cpdef int enable_mmap(self) except -1:
         """
         Switches to mmap-based file access method for the entire series,
@@ -94,6 +108,7 @@ cdef class TimeSeries:
             self.max_entries_per_chunk = metadata['max_entries_per_chunk']
             self.last_entry_synced = metadata['last_entry_synced']
             self.page_size = metadata.get('page_size', DEFAULT_PAGE_SIZE)
+            self.metadata = metadata.get('metadata')
         except (OSError, ValueError) as e:
             raise Corruption('Corrupted series: %s' % (e, ))
         except KeyError:
@@ -315,6 +330,8 @@ cdef class TimeSeries:
             }
         if self.page_size != DEFAULT_PAGE_SIZE:
             meta['page_size'] = self.page_size
+        if self.metadata is not None:
+            meta['metadata'] = self.metadata
         return meta
 
     cdef void register_memory_pressure_manager(self, object mpm):