diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb
index 21a79f892681542e7ba9f1ba81093b8365b0f15e..56bf69c4021e639624ba7e49536a1656e30aae94 100644
--- a/app/controllers/api/base_controller.rb
+++ b/app/controllers/api/base_controller.rb
@@ -6,6 +6,7 @@ class Api::BaseController < ApplicationController
 
   include RateLimitHeaders
   include AccessTokenTrackingConcern
+  include ApiCachingConcern
 
   skip_before_action :store_current_location
   skip_before_action :require_functional!, unless: :whitelist_mode?
@@ -13,6 +14,8 @@ class Api::BaseController < ApplicationController
   before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
   before_action :require_not_suspended!
 
+  vary_by 'Authorization'
+
   protect_from_forgery with: :null_session
 
   content_security_policy do |p|
diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
index 68952de893b6bc73a14deda976ed5398593cfe46..1a996d362aa4f4d71b76eef7c5d0c87df8dd9c5f 100644
--- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb
@@ -6,6 +6,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
   after_action :insert_pagination_headers
 
   def index
+    cache_if_unauthenticated!
     @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
   end
diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb
index 0a4d2ae7b308c7d872237616ed923c09a375cb5e..6e6ebae43b005c1adb621153d8b291ea3d3c2d68 100644
--- a/app/controllers/api/v1/accounts/following_accounts_controller.rb
+++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb
@@ -6,6 +6,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
   after_action :insert_pagination_headers
 
   def index
+    cache_if_unauthenticated!
     @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
   end
diff --git a/app/controllers/api/v1/accounts/lookup_controller.rb b/app/controllers/api/v1/accounts/lookup_controller.rb
index 8597f891d6d04c20748c8552d5e7ca1ad5d27201..6d63398781c7389f2fd38810a8a53e0bca86135d 100644
--- a/app/controllers/api/v1/accounts/lookup_controller.rb
+++ b/app/controllers/api/v1/accounts/lookup_controller.rb
@@ -5,6 +5,7 @@ class Api::V1::Accounts::LookupController < Api::BaseController
   before_action :set_account
 
   def show
+    cache_if_unauthenticated!
     render json: @account, serializer: REST::AccountSerializer
   end
 
diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb
index 7ed48cf6588a555efed7afbb16c4b6d280c5210e..51f541bd23ebc948294ac9c375e239ebdcc451ae 100644
--- a/app/controllers/api/v1/accounts/statuses_controller.rb
+++ b/app/controllers/api/v1/accounts/statuses_controller.rb
@@ -7,6 +7,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
   after_action :insert_pagination_headers, unless: -> { truthy_param?(:pinned) }
 
   def index
+    cache_if_unauthenticated!
     @statuses = load_statuses
     render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
   end
diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb
index 7dff66efac95c5643e6ea73fe6a92f19e026c4cb..8af4242ba30e232021120bd0ff1f858e08e26886 100644
--- a/app/controllers/api/v1/accounts_controller.rb
+++ b/app/controllers/api/v1/accounts_controller.rb
@@ -18,6 +18,7 @@ class Api::V1::AccountsController < Api::BaseController
   override_rate_limit_headers :follow, family: :follows
 
   def show
+    cache_if_unauthenticated!
     render json: @account, serializer: REST::AccountSerializer
   end
 
diff --git a/app/controllers/api/v1/custom_emojis_controller.rb b/app/controllers/api/v1/custom_emojis_controller.rb
index 380dbe8bf362103387f0b58f16a680f4848ed9ca..d4e7c43cb0af63f6112b87d75edc57020945006e 100644
--- a/app/controllers/api/v1/custom_emojis_controller.rb
+++ b/app/controllers/api/v1/custom_emojis_controller.rb
@@ -1,8 +1,10 @@
 # frozen_string_literal: true
 
 class Api::V1::CustomEmojisController < Api::BaseController
+  vary_by ''
+
   def index
-    expires_in 3.minutes, public: true
+    cache_even_if_authenticated!
     render_with_cache(each_serializer: REST::CustomEmojiSerializer) { CustomEmoji.listed.includes(:category) }
   end
 end
diff --git a/app/controllers/api/v1/directories_controller.rb b/app/controllers/api/v1/directories_controller.rb
index c91543e3a3e969610a5b2d0b8a1c55808fcd1869..c0585e8599a7148c8654150fc08fd0779be3bc53 100644
--- a/app/controllers/api/v1/directories_controller.rb
+++ b/app/controllers/api/v1/directories_controller.rb
@@ -5,6 +5,7 @@ class Api::V1::DirectoriesController < Api::BaseController
   before_action :set_accounts
 
   def show
+    cache_if_unauthenticated!
     render json: @accounts, each_serializer: REST::AccountSerializer
   end
 
diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb
index 7ccfec703649d51b6114a53c5340fb89e288ed68..3d55d990af2faec12b649ee10032d4aa8fee1f92 100644
--- a/app/controllers/api/v1/instances/activity_controller.rb
+++ b/app/controllers/api/v1/instances/activity_controller.rb
@@ -5,8 +5,10 @@ class Api::V1::Instances::ActivityController < Api::BaseController
 
   skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
 
+  vary_by ''
+
   def show
-    expires_in 1.day, public: true
+    cache_even_if_authenticated!
     render_with_cache json: :activity, expires_in: 1.day
   end
 
diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb
index 37a6906fb6b5c13e0f316561472437aacb0f5a71..49fd8fa987544e63b28401a997dfe677cb2a7b5f 100644
--- a/app/controllers/api/v1/instances/domain_blocks_controller.rb
+++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb
@@ -6,8 +6,10 @@ class Api::V1::Instances::DomainBlocksController < Api::BaseController
   before_action :require_enabled_api!
   before_action :set_domain_blocks
 
+  vary_by ''
+
   def index
-    expires_in 3.minutes, public: true
+    cache_even_if_authenticated!
     render json: @domain_blocks, each_serializer: REST::DomainBlockSerializer, with_comment: (Setting.show_domain_blocks_rationale == 'all' || (Setting.show_domain_blocks_rationale == 'users' && user_signed_in?))
   end
 
diff --git a/app/controllers/api/v1/instances/extended_descriptions_controller.rb b/app/controllers/api/v1/instances/extended_descriptions_controller.rb
index c72e16cff2c8bc0d99573aef726d4fe6630b3f35..17cf0d790daf433f609da83e37a22de3d931c021 100644
--- a/app/controllers/api/v1/instances/extended_descriptions_controller.rb
+++ b/app/controllers/api/v1/instances/extended_descriptions_controller.rb
@@ -5,8 +5,10 @@ class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController
 
   before_action :set_extended_description
 
+  vary_by ''
+
   def show
-    expires_in 3.minutes, public: true
+    cache_even_if_authenticated!
     render json: @extended_description, serializer: REST::ExtendedDescriptionSerializer
   end
 
diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb
index b2669a84eb7bd6dc6bf4775a77de39c9416d0fd7..20809d75533cc0e38b326f738ff48d12979c5c63 100644
--- a/app/controllers/api/v1/instances/peers_controller.rb
+++ b/app/controllers/api/v1/instances/peers_controller.rb
@@ -5,8 +5,10 @@ class Api::V1::Instances::PeersController < Api::BaseController
 
   skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
 
+  vary_by ''
+
   def index
-    expires_in 1.day, public: true
+    cache_even_if_authenticated!
     render_with_cache(expires_in: 1.day) { Instance.where.not(domain: DomainBlock.select(:domain)).pluck(:domain) }
   end
 
diff --git a/app/controllers/api/v1/instances/privacy_policies_controller.rb b/app/controllers/api/v1/instances/privacy_policies_controller.rb
index dbd69f54d4ab481da5a22640cb85851cec8353f5..36889f73354eb9b936dbb48e115794ea23887c97 100644
--- a/app/controllers/api/v1/instances/privacy_policies_controller.rb
+++ b/app/controllers/api/v1/instances/privacy_policies_controller.rb
@@ -5,8 +5,10 @@ class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController
 
   before_action :set_privacy_policy
 
+  vary_by ''
+
   def show
-    expires_in 1.day, public: true
+    cache_even_if_authenticated!
     render json: @privacy_policy, serializer: REST::PrivacyPolicySerializer
   end
 
diff --git a/app/controllers/api/v1/instances/rules_controller.rb b/app/controllers/api/v1/instances/rules_controller.rb
index 93cf3c75943d34f9e9cb3ce9c3e891e3d8dfc003..cd5cc7b0867163e47d3482b0ac839805668bd2e8 100644
--- a/app/controllers/api/v1/instances/rules_controller.rb
+++ b/app/controllers/api/v1/instances/rules_controller.rb
@@ -5,7 +5,10 @@ class Api::V1::Instances::RulesController < Api::BaseController
 
   before_action :set_rules
 
+  vary_by ''
+
   def index
+    cache_even_if_authenticated!
     render json: @rules, each_serializer: REST::RuleSerializer
   end
 
diff --git a/app/controllers/api/v1/instances/translation_languages_controller.rb b/app/controllers/api/v1/instances/translation_languages_controller.rb
index 3910a499e82f8ef3b5c07db48dfba42d96901614..c4680cccb8a7b503ff1af4061cb049222c1a29ec 100644
--- a/app/controllers/api/v1/instances/translation_languages_controller.rb
+++ b/app/controllers/api/v1/instances/translation_languages_controller.rb
@@ -5,8 +5,10 @@ class Api::V1::Instances::TranslationLanguagesController < Api::BaseController
 
   before_action :set_languages
 
+  vary_by ''
+
   def show
-    expires_in 1.day, public: true
+    cache_even_if_authenticated!
     render json: @languages
   end
 
diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb
index 3cdb404cb8131f2448c5925e12619d6a710f36e1..d4c822e64ae369798e1341e8f083e369db2bfe02 100644
--- a/app/controllers/api/v1/instances_controller.rb
+++ b/app/controllers/api/v1/instances_controller.rb
@@ -3,8 +3,10 @@
 class Api::V1::InstancesController < Api::BaseController
   skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
 
+  vary_by ''
+
   def show
-    expires_in 3.minutes, public: true
+    cache_even_if_authenticated!
     render_with_cache json: InstancePresenter.new, serializer: REST::V1::InstanceSerializer, root: 'instance'
   end
 end
diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb
index 6435e9f0dcde02d6cd563c57d2f08bfdb92ae74e..ffc70a84963b64647a396d5aea2812e19627fa21 100644
--- a/app/controllers/api/v1/polls_controller.rb
+++ b/app/controllers/api/v1/polls_controller.rb
@@ -8,6 +8,7 @@ class Api::V1::PollsController < Api::BaseController
   before_action :refresh_poll
 
   def show
+    cache_if_unauthenticated!
     render json: @poll, serializer: REST::PollSerializer, include_results: true
   end
 
diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
index b138fa265080112bbd53fd88db4bc4a2e2a5eca2..73eb11e711cc77e7349ae3f4ad468727386bcd35 100644
--- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb
@@ -8,6 +8,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
   after_action :insert_pagination_headers
 
   def index
+    cache_if_unauthenticated!
     @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
   end
diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb
index 7fe73a6f5493ccf59813b99c31a197e1a7d79785..dff2425d06228c096249335b63ab485e14454c84 100644
--- a/app/controllers/api/v1/statuses/histories_controller.rb
+++ b/app/controllers/api/v1/statuses/histories_controller.rb
@@ -7,6 +7,7 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController
   before_action :set_status
 
   def show
+    cache_if_unauthenticated!
     render json: @status.edits.includes(:account, status: [:account]), each_serializer: REST::StatusEditSerializer
   end
 
diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
index 4b545f9826ef6a08673b57db8378d951608a7418..41672e753901c5f1dd87bf1bd7c734581b802b01 100644
--- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb
@@ -8,6 +8,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
   after_action :insert_pagination_headers
 
   def index
+    cache_if_unauthenticated!
     @accounts = load_accounts
     render json: @accounts, each_serializer: REST::AccountSerializer
   end
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index fadd1b0451e8a4ee27d4d7765c4317125b4d98f9..064e7632a8009ae38f8f018aa5c1901c1675d477 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -24,11 +24,14 @@ class Api::V1::StatusesController < Api::BaseController
   DESCENDANTS_DEPTH_LIMIT = 20
 
   def show
+    cache_if_unauthenticated!
     @status = cache_collection([@status], Status).first
     render json: @status, serializer: REST::StatusSerializer
   end
 
   def context
+    cache_if_unauthenticated!
+
     ancestors_limit         = CONTEXT_LIMIT
     descendants_limit       = CONTEXT_LIMIT
     descendants_depth_limit = nil
diff --git a/app/controllers/api/v1/tags_controller.rb b/app/controllers/api/v1/tags_controller.rb
index a08fd21877a029c37df56f5549a6ebc8b05cf16d..284ec85937dc6ad2d6046f11648cfd39d50bf0b2 100644
--- a/app/controllers/api/v1/tags_controller.rb
+++ b/app/controllers/api/v1/tags_controller.rb
@@ -8,6 +8,7 @@ class Api::V1::TagsController < Api::BaseController
   override_rate_limit_headers :follow, family: :follows
 
   def show
+    cache_if_unauthenticated!
     render json: @tag, serializer: REST::TagSerializer
   end
 
diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb
index d253b744f99bf9d24ff29369b3f4e307cbec1217..5bbd92b9eecfb0958398ff65d368487723a288b9 100644
--- a/app/controllers/api/v1/timelines/public_controller.rb
+++ b/app/controllers/api/v1/timelines/public_controller.rb
@@ -5,6 +5,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController
   after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
 
   def show
+    cache_if_unauthenticated!
     @statuses = load_statuses
     render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
   end
diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb
index 64a1db58df3ae76094012d1789ad0d3b1c97a84f..9cd7b990467f0b24bb18a23a16d1bf2cab8bf7b5 100644
--- a/app/controllers/api/v1/timelines/tag_controller.rb
+++ b/app/controllers/api/v1/timelines/tag_controller.rb
@@ -5,6 +5,7 @@ class Api::V1::Timelines::TagController < Api::BaseController
   after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
 
   def show
+    cache_if_unauthenticated!
     @statuses = load_statuses
     render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
   end
diff --git a/app/controllers/api/v1/trends/links_controller.rb b/app/controllers/api/v1/trends/links_controller.rb
index 3ce20fb78625f77570c4f3317f2fa27b32cacc58..57cfa0b7e43b3092204756816e221d9e6bda365c 100644
--- a/app/controllers/api/v1/trends/links_controller.rb
+++ b/app/controllers/api/v1/trends/links_controller.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class Api::V1::Trends::LinksController < Api::BaseController
+  vary_by 'Authorization, Accept-Language'
+
   before_action :set_links
 
   after_action :insert_pagination_headers
@@ -8,6 +10,7 @@ class Api::V1::Trends::LinksController < Api::BaseController
   DEFAULT_LINKS_LIMIT = 10
 
   def index
+    cache_if_unauthenticated!
     render json: @links, each_serializer: REST::Trends::LinkSerializer
   end
 
diff --git a/app/controllers/api/v1/trends/statuses_controller.rb b/app/controllers/api/v1/trends/statuses_controller.rb
index 3aab92477de076bb5a0cdeba813145080d03c5bd..c186864c3b1e217f44ee8f6b7386e6e6db2f4395 100644
--- a/app/controllers/api/v1/trends/statuses_controller.rb
+++ b/app/controllers/api/v1/trends/statuses_controller.rb
@@ -1,11 +1,14 @@
 # frozen_string_literal: true
 
 class Api::V1::Trends::StatusesController < Api::BaseController
+  vary_by 'Authorization, Accept-Language'
+
   before_action :set_statuses
 
   after_action :insert_pagination_headers
 
   def index
+    cache_if_unauthenticated!
     render json: @statuses, each_serializer: REST::StatusSerializer
   end
 
diff --git a/app/controllers/api/v1/trends/tags_controller.rb b/app/controllers/api/v1/trends/tags_controller.rb
index 75c3ed218bc8a1188e06df039282a0cf46f007bf..aca3dd7089c1bcee790b53aa952912d5e9fedf28 100644
--- a/app/controllers/api/v1/trends/tags_controller.rb
+++ b/app/controllers/api/v1/trends/tags_controller.rb
@@ -8,6 +8,7 @@ class Api::V1::Trends::TagsController < Api::BaseController
   DEFAULT_TAGS_LIMIT = 10
 
   def index
+    cache_if_unauthenticated!
     render json: @tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@tags, current_user&.account_id)
   end
 
diff --git a/app/controllers/api/v2/instances_controller.rb b/app/controllers/api/v2/instances_controller.rb
index bcd90cff22e6aefb793017d998a85884fe3810ad..8346e28830a579da692f6615f7ab3ec1d1459eba 100644
--- a/app/controllers/api/v2/instances_controller.rb
+++ b/app/controllers/api/v2/instances_controller.rb
@@ -2,7 +2,7 @@
 
 class Api::V2::InstancesController < Api::V1::InstancesController
   def show
-    expires_in 3.minutes, public: true
+    cache_even_if_authenticated!
     render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance'
   end
 end
diff --git a/app/controllers/concerns/api_caching_concern.rb b/app/controllers/concerns/api_caching_concern.rb
new file mode 100644
index 0000000000000000000000000000000000000000..705abce80fdd64dcf867f49cd788b434553eab4c
--- /dev/null
+++ b/app/controllers/concerns/api_caching_concern.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ApiCachingConcern
+  extend ActiveSupport::Concern
+
+  def cache_if_unauthenticated!
+    expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless user_signed_in?
+  end
+
+  def cache_even_if_authenticated!
+    expires_in(5.minutes, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless whitelist_mode?
+  end
+end