diff --git a/lib/file_system.ex b/lib/file_system.ex
index 691faa07f89d34091c5d41d8dc81f0d6064e320f..23f98aee99c7e2c85ffff67fdb0602dfbbdc297c 100644
--- a/lib/file_system.ex
+++ b/lib/file_system.ex
@@ -4,7 +4,7 @@ defmodule FileSystem do
   @doc """
   ## Options
 
-    * `:dirs` ([string], requires), the dir list to monitor
+    * `:dirs` ([string], required), the dir list to monitor
 
     * `:backend` (atom, optional), default backends: `:fs_mac`
       for `macos`, `:fs_inotify` for `linux` and `freebsd`,
@@ -45,7 +45,7 @@ defmodule FileSystem do
       {:file_event, worker_pid, {file_path, events}}
       {:file_event, worker_pid, :stop}
   """
-  @spec subscribe(pid() | atom()) :: :ok
+  @spec subscribe(GenServer.server) :: :ok
   def subscribe(pid) do
     GenServer.call(pid, :subscribe)
   end
diff --git a/lib/file_system/backend.ex b/lib/file_system/backend.ex
index 7d397242c350f37c9fbfd66c9ce763a5357706e5..3fe5d2cc0e1fa3ca7d62723c925b9f7f7a174294 100644
--- a/lib/file_system/backend.ex
+++ b/lib/file_system/backend.ex
@@ -40,6 +40,7 @@ defmodule FileSystem.Backend do
   defp backend_module(:fs_mac),     do: {:ok, FileSystem.Backends.FSMac}
   defp backend_module(:fs_inotify), do: {:ok, FileSystem.Backends.FSInotify}
   defp backend_module(:fs_windows), do: {:ok, FileSystem.Backends.FSWindows}
+  defp backend_module(:fs_poll),    do: {:ok, FileSystem.Backends.FSPoll}
   defp backend_module({:unsupported_system, system}) do
     Logger.error "I'm so sorry but `file_system` does NOT support your current system #{inspect system} for now."
     {:error, :unsupported_system}
diff --git a/lib/file_system/backends/fs_poll.ex b/lib/file_system/backends/fs_poll.ex
new file mode 100644
index 0000000000000000000000000000000000000000..826b39cbc4f0e1b668dcd849d0b99cb8624f5b19
--- /dev/null
+++ b/lib/file_system/backends/fs_poll.ex
@@ -0,0 +1,106 @@
+require Logger
+
+defmodule FileSystem.Backends.FSPoll do
+  @moduledoc """
+  FileSysetm backend for any OS, a GenServer that regularly scans file system to
+  detect changes and send them to the worker process.
+
+  ## Backend Options
+
+    * `:interval` (integer, default: 1000), polling interval
+
+  ## Use FSPoll Backend
+
+  Unlike other backends, polling backend is never automatically chosen in any
+  OS environment, despite being usable on all platforms.
+
+  To use polling backend, one has to explicitly specify in the backend option.
+  """
+
+  use GenServer
+  @behaviour FileSystem.Backend
+
+  def bootstrap, do: :ok
+
+  def supported_systems do
+    [{:unix, :linux}, {:unix, :freebsd}, {:unix, :darwin}, {:win32, :nt}]
+  end
+
+  def known_events do
+    [:created, :deleted, :modified]
+  end
+
+  def start_link(args) do
+    GenServer.start_link(__MODULE__, args, [])
+  end
+
+  def init(args) do
+    worker_pid = Keyword.fetch!(args, :worker_pid)
+    dirs = Keyword.fetch!(args, :dirs)
+    interval = Keyword.get(args, :interval, 1000)
+
+    Logger.info("Polling file changes every #{interval}ms...")
+    send(self(), :first_check)
+
+    {:ok, {worker_pid, dirs, interval, %{}}}
+  end
+
+  def handle_info(:first_check, {worker_pid, dirs, interval, _empty_map}) do
+    schedule_check(interval)
+    {:noreply, {worker_pid, dirs, interval, files_mtimes(dirs)}}
+  end
+
+  def handle_info(:check, {worker_pid, dirs, interval, stale_mtimes}) do
+    fresh_mtimes = files_mtimes(dirs)
+
+    diff(stale_mtimes, fresh_mtimes)
+    |> Tuple.to_list
+    |> Enum.zip([:created, :deleted, :modified])
+    |> Enum.each(&report_change(&1, worker_pid))
+
+    schedule_check(interval)
+    {:noreply, {worker_pid, dirs, interval, fresh_mtimes}}
+  end
+
+  defp schedule_check(interval) do
+    Process.send_after(self(), :check, interval)
+  end
+
+  defp files_mtimes(dirs, files_mtimes_map \\ %{}) do
+    Enum.reduce(dirs, files_mtimes_map, fn dir, map ->
+      case File.stat!(dir) do
+        %{type: :regular, mtime: mtime} ->
+          Map.put(map, dir, mtime)
+        %{type: :directory} ->
+          dir
+          |> Path.join("*")
+          |> Path.wildcard
+          |> files_mtimes(map)
+        %{type: _other} ->
+          map
+      end
+    end)
+  end
+
+  @doc false
+  def diff(stale_mtimes, fresh_mtimes) do
+    fresh_file_paths = fresh_mtimes |> Map.keys |> MapSet.new
+    stale_file_paths = stale_mtimes |> Map.keys |> MapSet.new
+
+    created_file_paths =
+      MapSet.difference(fresh_file_paths, stale_file_paths) |> MapSet.to_list
+    deleted_file_paths =
+      MapSet.difference(stale_file_paths, fresh_file_paths) |> MapSet.to_list
+    modified_file_paths =
+      for file_path <- MapSet.intersection(stale_file_paths, fresh_file_paths),
+        stale_mtimes[file_path] != fresh_mtimes[file_path], do: file_path
+
+    {created_file_paths, deleted_file_paths, modified_file_paths}
+  end
+
+  defp report_change({file_paths, event}, worker_pid) do
+    for file_path <- file_paths do
+      send(worker_pid, {:backend_file_event, self(), {file_path, [event]}})
+    end
+  end
+end
diff --git a/test/backends/fs_poll_test.exs b/test/backends/fs_poll_test.exs
new file mode 100644
index 0000000000000000000000000000000000000000..86d9d5316c25ef19b9eaf8b104075eb976223bdb
--- /dev/null
+++ b/test/backends/fs_poll_test.exs
@@ -0,0 +1,34 @@
+defmodule FileSystem.Backends.FSPollTest do
+  use ExUnit.Case, async: true
+  import FileSystem.Backends.FSPoll
+
+  @mtime1 {{2017, 11, 13}, {10, 14, 00}}
+  @mtime2 {{2017, 11, 13}, {10, 15, 00}}
+
+  @stale %{
+    "modified" => @mtime1,
+    "deleted" => @mtime1,
+  }
+
+  @fresh %{
+    "created" => @mtime2,
+    "modified" => @mtime2
+  }
+
+  describe "diff" do
+    test "detect created file" do
+      {created, _, _} = diff(@stale, @fresh)
+      created = ["created"]
+    end
+
+    test "detect modified file" do
+      {_, modified, _} = diff(@stale, @fresh)
+      modified = ["modified"]
+    end
+
+    test "detect deleted file" do
+      {_, _, deleted} = diff(@stale, @fresh)
+      deleted = ["deleted"]
+    end
+  end
+end