diff --git a/src/lib/core/coio_task.c b/src/lib/core/coio_task.c
index d8095eef3eabf8ec5d36b0eb753171725c3721f1..908b336ed15e1430570308853769dfc3d8f27b65 100644
--- a/src/lib/core/coio_task.c
+++ b/src/lib/core/coio_task.c
@@ -227,22 +227,22 @@ coio_task_destroy(struct coio_task *task)
 	diag_destroy(&task->diag);
 }
 
+void
+coio_task_post(struct coio_task *task)
+{
+	assert(task->base.type == EIO_CUSTOM);
+	assert(task->fiber == fiber());
+	eio_submit(&task->base);
+	task->fiber = NULL;
+}
+
 int
-coio_task_post(struct coio_task *task, double timeout)
+coio_task_execute(struct coio_task *task, double timeout)
 {
 	assert(task->base.type == EIO_CUSTOM);
 	assert(task->fiber == fiber());
 
 	eio_submit(&task->base);
-	if (timeout == 0) {
-		/*
-		* This is a special case:
-		* we don't wait any response from the task
-		* and just perform just asynchronous post.
-		*/
-		task->fiber = NULL;
-		return 0;
-	}
 	fiber_yield_timeout(timeout);
 	if (!task->complete) {
 		/* timed out or cancelled. */
@@ -409,7 +409,7 @@ coio_getaddrinfo(const char *host, const char *port,
 	}
 
 	/* Post coio task */
-	if (coio_task_post(&task->base, timeout) != 0)
+	if (coio_task_execute(&task->base, timeout) != 0)
 		return -1; /* timed out or cancelled */
 
 	/* Task finished */
diff --git a/src/lib/core/coio_task.h b/src/lib/core/coio_task.h
index bb981f0b0998d0975fe1bc6f8e42efc9ce6f2d79..e81e10ccafaf5d8e754a788648a475d11d930600 100644
--- a/src/lib/core/coio_task.h
+++ b/src/lib/core/coio_task.h
@@ -63,7 +63,11 @@ typedef int (*coio_task_cb)(struct coio_task *task); /* like eio_req */
  */
 struct coio_task {
 	struct eio_req base; /* eio_task - must be first */
-	/** The calling fiber. */
+	/**
+	 * The calling fiber. When set to NULL, the task is
+	 * detached - its resources are freed eventually, and such
+	 * a task should not be accessed after detachment.
+	 */
 	struct fiber *fiber;
 	/** Callbacks. */
 	union {
@@ -102,7 +106,7 @@ void
 coio_task_destroy(struct coio_task *task);
 
 /**
- * Post coio task to EIO thread pool.
+ * Execute a coio task in a worker thread.
  *
  * @param task coio task.
  * @param timeout timeout in seconds.
@@ -114,7 +118,14 @@ coio_task_destroy(struct coio_task *task);
  *            callback.
  */
 int
-coio_task_post(struct coio_task *task, double timeout);
+coio_task_execute(struct coio_task *task, double timeout);
+
+/**
+ * Post a task in detached state. Its result can't be obtained,
+ * and destructor is called after completion.
+ */
+void
+coio_task_post(struct coio_task *task);
 
 /** \cond public */
 
diff --git a/src/lib/core/say.c b/src/lib/core/say.c
index 24963cf82cb695e09b3da6b6371146288dab24a8..0b2cf2c34b322a9c0a915f7b0e27570ef7506bc3 100644
--- a/src/lib/core/say.c
+++ b/src/lib/core/say.c
@@ -343,7 +343,7 @@ say_logrotate(struct ev_loop *loop, struct ev_signal *w, int revents)
 		coio_task_create(&task->base, logrotate_cb, logrotate_cleanup_cb);
 		task->log = log;
 		task->loop = loop();
-		coio_task_post(&task->base, 0);
+		coio_task_post(&task->base);
 	}
 	errno = saved_errno;
 }
diff --git a/test/unit/coio.cc b/test/unit/coio.cc
index 5fb5a26d51c334e56261b009fb1e323c4c9e5110..bb8bd713120041475755b6e30e5a1701b8ed83a0 100644
--- a/test/unit/coio.cc
+++ b/test/unit/coio.cc
@@ -80,6 +80,24 @@ test_getaddrinfo(void)
 	int rc = coio_getaddrinfo(host, port, NULL, &i, 1);
 	is(rc, 0, "getaddrinfo");
 	freeaddrinfo(i);
+
+	/*
+	 * gh-4209: 0 timeout should not be a special value and
+	 * detach a task. Before a fix it led to segfault
+	 * sometimes. The cycle below runs getaddrinfo multiple
+	 * times to increase segfault probability.
+	 */
+	for (int j = 0; j < 5; ++j) {
+		if (coio_getaddrinfo(host, port, NULL, &i, 0) == 0 && i != NULL)
+			freeaddrinfo(i);
+		/*
+		 * Skip one event loop to check, that the coio
+		 * task destructor will not free the memory second
+		 * time.
+		 */
+		fiber_sleep(0);
+	}
+
 	check_plan();
 	footer();
 }