diff --git a/daemon/README.rst b/daemon/README.rst
index 943b87c13e260367a47e4bb9ae840fbf9c6c5332..dff2e49417f1b93b58bd624d5def87c42d29f808 100644
--- a/daemon/README.rst
+++ b/daemon/README.rst
@@ -153,20 +153,56 @@ to download cache from parent, to avoid cold-cache start.
 Events and services
 ^^^^^^^^^^^^^^^^^^^
 
-The Lua supports a concept called closures, this is extremely useful for scripting actions upon various events.
+The Lua supports a concept called closures_, this is extremely useful for scripting actions upon various events,
+say for example - prune the cache within minute after loading, publish statistics each 5 minutes and so on.
+Here's an example of an anonymous function with :func:`event.recurrent()`:
 
-.. note:: Work in progress, come back later!
+.. code-block:: lua
+
+	-- every 5 minutes
+	event.recurrent(5 * minute, function()
+		cachectl.prune()
+	end)
+
+Note that each scheduled event is identified by a number valid for the duration of the event,
+you may cancel it at any time. You can do this with anonymous functions, if you accept the event
+as a parameter, but it's not very useful as you don't have any *non-global* way to keep persistent variables.
+
+.. code-block:: lua
+
+	-- make a closure, encapsulating counter
+	function pruner()
+		local i = 0
+		-- pruning function
+		return function(e)
+			cachectl.prune()
+			-- cancel event on 5th attempt
+			i = i + 1
+			if i == 5 then
+				event.cancel(e)
+			fi
+		end
+	end
+
+	-- make recurrent event that will cancel after 5 times
+	event.recurrent(5 * minute, pruner())
 
-* Timers and events
 * File watchers
 * Data I/O
 
+.. note:: Work in progress, come back later!
+
+.. _closures: http://www.lua.org/pil/6.1.html
 
 Configuration reference
 -----------------------
 
 This is a reference for variables and functions available to both configuration file and CLI.
 
+.. contents::
+   :depth: 1
+   :local:
+
 Environment
 ^^^^^^^^^^^
 
@@ -319,6 +355,56 @@ daemons or manipulated from other processes, making for example synchronised loa
 
    Close the cache.
 
+
+Timers and events
+^^^^^^^^^^^^^^^^^
+
+The timer represents exactly the thing described in the examples - it allows you to execute closures after specified time,
+or event recurrent events. Time is always described in miliseconds, but there are convenient variables that you can use -
+``sec, minute, hour``. For example, ``5 * hour`` represents five hours, or 5*60*60*100 milliseconds.
+
+.. function:: event.after(time, function)
+
+   :return: event id
+
+   Execute function after the specified time has passed.
+   The first parameter of the callback is the event itself.
+
+   Example:
+
+   .. code-block:: lua
+
+	event.after(1 * minute, function() print('Hi!') end)
+
+.. function:: event.recurrent(interval, function)
+
+   :return: event id
+
+   Similar to :func:`event.after()`, periodically execute function after ``interval`` passes. 
+
+   Example:
+
+   .. code-block:: lua
+
+	msg_count = 0
+	event.recurrent(5 * sec, function(e) 
+		msg_count = msg_count + 1
+		print('Hi #'..msg_count)
+	end)
+
+.. function:: event.cancel(event_id)
+
+   Cancel running event, it has no effect on already canceled events.
+   New events may reuse the event_id, so the behaviour is undefined if the function
+   is called after another event is started.
+
+   Example:
+
+   .. code-block:: lua
+
+	e = event.after(1 * minute, function() print('Hi!') end)
+	event.cancel(e)
+
 .. _`JSON-encoded`: http://json.org/example
 .. _`Learn Lua in 15 minutes`: http://tylerneylon.com/a/learn-lua/
 .. _`PowerDNS Recursor`: https://doc.powerdns.com/md/recursor/scripting/
diff --git a/daemon/bindings.c b/daemon/bindings.c
index dc677b3e4421786ebf9b11e17d4973043f568c40..d685d1fe7ad651d0272eba5ba4ab1ff39be4e9dd 100644
--- a/daemon/bindings.c
+++ b/daemon/bindings.c
@@ -18,6 +18,7 @@
 
 #include "lib/cache.h"
 #include "daemon/bindings.h"
+#include "daemon/worker.h"
 
 /** @internal Prefix error with file:line */
 static int format_error(lua_State* L, const char *err)
@@ -355,3 +356,119 @@ int lib_cache(lua_State *L)
 	register_lib(L, "cache", lib);
 	return 1;
 }
+
+static void event_free(uv_timer_t *timer)
+{
+	struct worker_ctx *worker = timer->loop->data;
+	lua_State *L = worker->engine->L;
+	int ref = (intptr_t) timer->data;
+	luaL_unref(L, LUA_REGISTRYINDEX, ref);
+	free(timer);
+}
+
+static void event_callback(uv_timer_t *timer)
+{
+	struct worker_ctx *worker = timer->loop->data;
+	lua_State *L = worker->engine->L;
+
+	/* Retrieve callback and execute */
+	lua_rawgeti(L, LUA_REGISTRYINDEX, (intptr_t) timer->data);
+	lua_rawgeti(L, -1, 1);
+	lua_pushinteger(L, (intptr_t) timer->data);
+	engine_pcall(L, 1);
+
+	/* Free callback if not recurrent */
+	if (uv_timer_get_repeat(timer) == 0) {
+		uv_close((uv_handle_t *)timer, (uv_close_cb) event_free);
+	}
+}
+
+static int event_sched(lua_State *L, unsigned timeout, unsigned repeat)
+{
+	uv_timer_t *timer = malloc(sizeof(*timer));
+	if (!timer) {
+		format_error(L, "out of memory");
+		lua_error(L);
+	}
+
+	/* Start timer with the reference */
+	uv_loop_t *loop = uv_default_loop();
+	uv_timer_init(loop, timer);
+	int ret = uv_timer_start(timer, event_callback, timeout, repeat);
+	if (ret != 0) {
+		free(timer);
+		format_error(L, "couldn't start the event");
+		lua_error(L);
+	}
+
+	/* Save callback and timer in registry */
+	lua_newtable(L);
+	lua_pushvalue(L, 2);
+	lua_rawseti(L, -2, 1);
+	lua_pushlightuserdata(L, timer);
+	lua_rawseti(L, -2, 2);
+	int ref = luaL_ref(L, LUA_REGISTRYINDEX);
+
+	/* Save reference to the timer */
+	timer->data = (void *) (intptr_t)ref;
+	lua_pushinteger(L, ref);
+	return 1;
+}
+
+static int event_after(lua_State *L)
+{
+	/* Check parameters */
+	int n = lua_gettop(L);
+	if (n < 2 || !lua_isnumber(L, 1) || !lua_isfunction(L, 2)) {
+		format_error(L, "expected 'after(number timeout, function)'");
+		lua_error(L);
+	}
+
+	return event_sched(L, lua_tonumber(L, 1), 0);
+}
+
+static int event_recurrent(lua_State *L)
+{
+	/* Check parameters */
+	int n = lua_gettop(L);
+	if (n < 2 || !lua_isnumber(L, 1) || !lua_isfunction(L, 2)) {
+		format_error(L, "expected 'recurrent(number interval, function)'");
+		lua_error(L);
+	}
+	return event_sched(L, 0, lua_tonumber(L, 1));
+}
+
+static int event_cancel(lua_State *L)
+{
+	int n = lua_gettop(L);
+	if (n < 1 || !lua_isnumber(L, 1)) {
+		format_error(L, "expected 'cancel(number event)'");
+		lua_error(L);
+	}
+
+	/* Fetch event if it exists */
+	lua_rawgeti(L, LUA_REGISTRYINDEX, lua_tointeger(L, 1));
+	if (!lua_istable(L, -1)) {
+		format_error(L, "event not exists");
+		lua_error(L);
+	}
+
+	/* Close the timer */
+	lua_rawgeti(L, -1, 2);
+	uv_handle_t *timer = lua_touserdata(L, -1);
+	uv_close(timer, (uv_close_cb) event_free);
+	return 0;
+}
+
+int lib_event(lua_State *L)
+{
+	static const luaL_Reg lib[] = {
+		{ "after",      event_after },
+		{ "recurrent",  event_recurrent },
+		{ "cancel",     event_cancel },
+		{ NULL, NULL }
+	};
+
+	register_lib(L, "event", lib);
+	return 1;
+}
diff --git a/daemon/bindings.h b/daemon/bindings.h
index c1ad1a286aff3fc64c580becc4afd8e72bbc10e6..b6e0b1d1d6f13a789d2400021f23a8b13aa621c7 100644
--- a/daemon/bindings.h
+++ b/daemon/bindings.h
@@ -44,4 +44,11 @@ int lib_net(lua_State *L);
  * @param  L scriptable
  * @return   number of packages to load
  */
-int lib_cache(lua_State *L);
\ No newline at end of file
+int lib_cache(lua_State *L);
+
+/**
+ * Load 'event' package.
+ * @param  L scriptable
+ * @return   number of packages to load
+ */
+int lib_event(lua_State *L);
\ No newline at end of file
diff --git a/daemon/main.c b/daemon/main.c
index f3f4fe547d22a06adccabd28b095baf6be415f5f..24d45963f0eec37ad6936906674fcb6758a25ba5 100644
--- a/daemon/main.c
+++ b/daemon/main.c
@@ -141,6 +141,7 @@ int main(int argc, char **argv)
 	engine_lualib(&engine, "modules", lib_modules);
 	engine_lualib(&engine, "net",     lib_net);
 	engine_lualib(&engine, "cache",   lib_cache);
+	engine_lualib(&engine, "event",   lib_event);
 
 	/* Create main worker. */
 	struct worker_ctx worker = {