diff --git a/CHANGELOG.md b/CHANGELOG.md
index b5c42d1fd..02d64a850 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,6 +41,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - OAuth: added support for refresh tokens
 - Emoji packs and emoji pack manager
 - Object pruning (`mix pleroma.database prune_objects`)
+- OAuth: added job to clean expired access tokens
 
 ### Changed
 - **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer
diff --git a/config/config.exs b/config/config.exs
index a05f8b1d2..33b7e713d 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -481,7 +481,9 @@ config :pleroma, Pleroma.ScheduledActivity,
 
 config :pleroma, :oauth2,
   token_expires_in: 600,
-  issue_new_refresh_token: true
+  issue_new_refresh_token: true,
+  clean_expired_tokens: false,
+  clean_expired_tokens_interval: 86_400_000
 
 config :pleroma, :database, rum_enabled: false
 
diff --git a/docs/config.md b/docs/config.md
index a050068f4..264b65499 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -550,6 +550,8 @@ Configure OAuth 2 provider capabilities:
 
 * `token_expires_in` - The lifetime in seconds of the access token.
 * `issue_new_refresh_token` - Keeps old refresh token or generate new refresh token when to obtain an access token.
+* `clean_expired_tokens` - Enable a background job to clean expired oauth tokens. Defaults to `false`.
+* `clean_expired_tokens_interval` - Interval to run the job to clean expired tokens. Defaults to `86_400_000` (24 hours).
 
 ## :emoji
 * `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]`
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index dab45a0b2..76df3945e 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -110,6 +110,7 @@ defmodule Pleroma.Application do
         hackney_pool_children() ++
         [
           worker(Pleroma.Web.Federator.RetryQueue, []),
+          worker(Pleroma.Web.OAuth.Token.CleanWorker, []),
           worker(Pleroma.Stats, []),
           worker(Task, [&Pleroma.Web.Push.init/0], restart: :temporary, id: :web_push_init),
           worker(Task, [&Pleroma.Web.Federator.init/0], restart: :temporary, id: :federator_init)
diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex
index 66c95c2e9..f412f7eb2 100644
--- a/lib/pleroma/web/oauth/token.ex
+++ b/lib/pleroma/web/oauth/token.ex
@@ -5,7 +5,6 @@
 defmodule Pleroma.Web.OAuth.Token do
   use Ecto.Schema
 
-  import Ecto.Query
   import Ecto.Changeset
 
   alias Pleroma.Repo
@@ -13,6 +12,7 @@ defmodule Pleroma.Web.OAuth.Token do
   alias Pleroma.Web.OAuth.App
   alias Pleroma.Web.OAuth.Authorization
   alias Pleroma.Web.OAuth.Token
+  alias Pleroma.Web.OAuth.Token.Query
 
   @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600)
   @type t :: %__MODULE__{}
@@ -31,17 +31,17 @@ defmodule Pleroma.Web.OAuth.Token do
   @doc "Gets token for app by access token"
   @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
   def get_by_token(%App{id: app_id} = _app, token) do
-    from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token)
+    Query.get_by_app(app_id)
+    |> Query.get_by_token(token)
     |> Repo.find_resource()
   end
 
   @doc "Gets token for app by refresh token"
   @spec get_by_refresh_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found}
   def get_by_refresh_token(%App{id: app_id} = _app, token) do
-    from(t in __MODULE__,
-      where: t.app_id == ^app_id and t.refresh_token == ^token,
-      preload: [:user]
-    )
+    Query.get_by_app(app_id)
+    |> Query.get_by_refresh_token(token)
+    |> Query.preload([:user])
     |> Repo.find_resource()
   end
 
@@ -97,29 +97,25 @@ defmodule Pleroma.Web.OAuth.Token do
   end
 
   def delete_user_tokens(%User{id: user_id}) do
-    from(
-      t in Token,
-      where: t.user_id == ^user_id
-    )
+    Query.get_by_user(user_id)
     |> Repo.delete_all()
   end
 
   def delete_user_token(%User{id: user_id}, token_id) do
-    from(
-      t in Token,
-      where: t.user_id == ^user_id,
-      where: t.id == ^token_id
-    )
+    Query.get_by_user(user_id)
+    |> Query.get_by_id(token_id)
+    |> Repo.delete_all()
+  end
+
+  def delete_expired_tokens do
+    Query.get_expired_tokens()
     |> Repo.delete_all()
   end
 
   def get_user_tokens(%User{id: user_id}) do
-    from(
-      t in Token,
-      where: t.user_id == ^user_id
-    )
+    Query.get_by_user(user_id)
+    |> Query.preload([:app])
     |> Repo.all()
-    |> Repo.preload(:app)
   end
 
   def is_expired?(%__MODULE__{valid_until: valid_until}) do
diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex
new file mode 100644
index 000000000..dca852449
--- /dev/null
+++ b/lib/pleroma/web/oauth/token/clean_worker.ex
@@ -0,0 +1,41 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.OAuth.Token.CleanWorker do
+  @moduledoc """
+  The module represents functions to clean an expired oauth tokens.
+  """
+
+  # 10 seconds
+  @start_interval 10_000
+  @interval Pleroma.Config.get(
+              # 24 hours
+              [:oauth2, :clean_expired_tokens_interval],
+              86_400_000
+            )
+  @queue :background
+
+  alias Pleroma.Web.OAuth.Token
+
+  def start_link, do: GenServer.start_link(__MODULE__, nil)
+
+  def init(_) do
+    if Pleroma.Config.get([:oauth2, :clean_expired_tokens], false) do
+      Process.send_after(self(), :perform, @start_interval)
+      {:ok, nil}
+    else
+      :ignore
+    end
+  end
+
+  @doc false
+  def handle_info(:perform, state) do
+    Process.send_after(self(), :perform, @interval)
+    PleromaJobQueue.enqueue(@queue, __MODULE__, [:clean])
+    {:noreply, state}
+  end
+
+  # Job Worker Callbacks
+  def perform(:clean), do: Token.delete_expired_tokens()
+end
diff --git a/lib/pleroma/web/oauth/token/query.ex b/lib/pleroma/web/oauth/token/query.ex
new file mode 100644
index 000000000..d92e1f071
--- /dev/null
+++ b/lib/pleroma/web/oauth/token/query.ex
@@ -0,0 +1,55 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.OAuth.Token.Query do
+  @moduledoc """
+  Contains queries for OAuth Token.
+  """
+
+  import Ecto.Query, only: [from: 2]
+
+  @type query :: Ecto.Queryable.t() | Token.t()
+
+  alias Pleroma.Web.OAuth.Token
+
+  @spec get_by_refresh_token(query, String.t()) :: query
+  def get_by_refresh_token(query \\ Token, refresh_token) do
+    from(q in query, where: q.refresh_token == ^refresh_token)
+  end
+
+  @spec get_by_token(query, String.t()) :: query
+  def get_by_token(query \\ Token, token) do
+    from(q in query, where: q.token == ^token)
+  end
+
+  @spec get_by_app(query, String.t()) :: query
+  def get_by_app(query \\ Token, app_id) do
+    from(q in query, where: q.app_id == ^app_id)
+  end
+
+  @spec get_by_id(query, String.t()) :: query
+  def get_by_id(query \\ Token, id) do
+    from(q in query, where: q.id == ^id)
+  end
+
+  @spec get_expired_tokens(query, DateTime.t() | nil) :: query
+  def get_expired_tokens(query \\ Token, date \\ nil) do
+    expired_date = date || Timex.now()
+    from(q in query, where: fragment("?", q.valid_until) < ^expired_date)
+  end
+
+  @spec get_by_user(query, String.t()) :: query
+  def get_by_user(query \\ Token, user_id) do
+    from(q in query, where: q.user_id == ^user_id)
+  end
+
+  @spec preload(query, any) :: query
+  def preload(query \\ Token, assoc_preload \\ [])
+
+  def preload(query, assoc_preload) when is_list(assoc_preload) do
+    from(q in query, preload: ^assoc_preload)
+  end
+
+  def preload(query, _assoc_preload), do: query
+end
diff --git a/test/web/oauth/token_test.exs b/test/web/oauth/token_test.exs
index ad2a49f09..3c07309b7 100644
--- a/test/web/oauth/token_test.exs
+++ b/test/web/oauth/token_test.exs
@@ -69,4 +69,17 @@ defmodule Pleroma.Web.OAuth.TokenTest do
 
     assert tokens == 2
   end
+
+  test "deletes expired tokens" do
+    insert(:oauth_token, valid_until: Timex.shift(Timex.now(), days: -3))
+    insert(:oauth_token, valid_until: Timex.shift(Timex.now(), days: -3))
+    t3 = insert(:oauth_token)
+    t4 = insert(:oauth_token, valid_until: Timex.shift(Timex.now(), minutes: 10))
+    {tokens, _} = Token.delete_expired_tokens()
+    assert tokens == 2
+    available_tokens = Pleroma.Repo.all(Token)
+
+    token_ids = available_tokens |> Enum.map(& &1.id)
+    assert token_ids == [t3.id, t4.id]
+  end
 end