diff --git a/compiler/rustc_middle/src/ty/util.rs b/compiler/rustc_middle/src/ty/util.rs
index eb903ebfd99..ba05135638e 100644
--- a/compiler/rustc_middle/src/ty/util.rs
+++ b/compiler/rustc_middle/src/ty/util.rs
@@ -518,6 +518,42 @@ impl<'tcx> TyCtxt<'tcx> {
         Ok(())
     }
 
+    /// Checks whether each generic argument is simply a unique generic placeholder.
+    ///
+    /// This is used in the new solver, which canonicalizes params to placeholders
+    /// for better caching.
+    pub fn uses_unique_placeholders_ignoring_regions(
+        self,
+        substs: SubstsRef<'tcx>,
+    ) -> Result<(), NotUniqueParam<'tcx>> {
+        let mut seen = GrowableBitSet::default();
+        for arg in substs {
+            match arg.unpack() {
+                // Ignore regions, since we can't resolve those in a canonicalized
+                // query in the trait solver.
+                GenericArgKind::Lifetime(_) => {}
+                GenericArgKind::Type(t) => match t.kind() {
+                    ty::Placeholder(p) => {
+                        if !seen.insert(p.bound.var) {
+                            return Err(NotUniqueParam::DuplicateParam(t.into()));
+                        }
+                    }
+                    _ => return Err(NotUniqueParam::NotParam(t.into())),
+                },
+                GenericArgKind::Const(c) => match c.kind() {
+                    ty::ConstKind::Placeholder(p) => {
+                        if !seen.insert(p.bound) {
+                            return Err(NotUniqueParam::DuplicateParam(c.into()));
+                        }
+                    }
+                    _ => return Err(NotUniqueParam::NotParam(c.into())),
+                },
+            }
+        }
+
+        Ok(())
+    }
+
     /// Returns `true` if `def_id` refers to a closure (e.g., `|x| x * 2`). Note
     /// that closures have a `DefId`, but the closure *expression* also
     /// has a `HirId` that is located within the context where the
diff --git a/compiler/rustc_trait_selection/src/solve/eval_ctxt.rs b/compiler/rustc_trait_selection/src/solve/eval_ctxt.rs
index bd83666eb1e..32a8bda2663 100644
--- a/compiler/rustc_trait_selection/src/solve/eval_ctxt.rs
+++ b/compiler/rustc_trait_selection/src/solve/eval_ctxt.rs
@@ -10,7 +10,8 @@ use rustc_infer::traits::query::NoSolution;
 use rustc_infer::traits::ObligationCause;
 use rustc_middle::infer::unify_key::{ConstVariableOrigin, ConstVariableOriginKind};
 use rustc_middle::traits::solve::{
-    CanonicalInput, Certainty, MaybeCause, PredefinedOpaques, PredefinedOpaquesData, QueryResult,
+    CanonicalInput, CanonicalResponse, Certainty, MaybeCause, PredefinedOpaques,
+    PredefinedOpaquesData, QueryResult,
 };
 use rustc_middle::traits::DefiningAnchor;
 use rustc_middle::ty::{
@@ -726,7 +727,12 @@ impl<'tcx> EvalCtxt<'_, 'tcx> {
         }
     }
 
-    pub(super) fn handle_opaque_ty(
+    pub(super) fn can_define_opaque_ty(&mut self, def_id: DefId) -> bool {
+        let Some(def_id) = def_id.as_local() else { return false; };
+        self.infcx.opaque_type_origin(def_id).is_some()
+    }
+
+    pub(super) fn register_opaque_ty(
         &mut self,
         a: Ty<'tcx>,
         b: Ty<'tcx>,
@@ -737,4 +743,42 @@ impl<'tcx> EvalCtxt<'_, 'tcx> {
         self.add_goals(obligations.into_iter().map(|obligation| obligation.into()));
         Ok(())
     }
+
+    // Do something for each opaque/hidden pair defined with `def_id` in the
+    // current inference context.
+    pub(super) fn unify_existing_opaque_tys(
+        &mut self,
+        param_env: ty::ParamEnv<'tcx>,
+        key: ty::AliasTy<'tcx>,
+        ty: Ty<'tcx>,
+    ) -> Vec<CanonicalResponse<'tcx>> {
+        let Some(def_id) = key.def_id.as_local() else { return vec![]; };
+
+        // FIXME: Super inefficient to be cloning this...
+        let opaques = self.infcx.clone_opaque_types_for_query_response();
+
+        let mut values = vec![];
+        for (candidate_key, candidate_ty) in opaques {
+            if candidate_key.def_id != def_id {
+                continue;
+            }
+            values.extend(self.probe(|ecx| {
+                for (a, b) in std::iter::zip(candidate_key.substs, key.substs) {
+                    ecx.eq(param_env, a, b)?;
+                }
+                ecx.eq(param_env, candidate_ty, ty)?;
+                let mut obl = vec![];
+                ecx.infcx.add_item_bounds_for_hidden_type(
+                    candidate_key,
+                    ObligationCause::dummy(),
+                    param_env,
+                    candidate_ty,
+                    &mut obl,
+                );
+                ecx.add_goals(obl.into_iter().map(Into::into));
+                ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+            }));
+        }
+        values
+    }
 }
diff --git a/compiler/rustc_trait_selection/src/solve/opaques.rs b/compiler/rustc_trait_selection/src/solve/opaques.rs
index 7d6e1647cfc..1d5bb46450c 100644
--- a/compiler/rustc_trait_selection/src/solve/opaques.rs
+++ b/compiler/rustc_trait_selection/src/solve/opaques.rs
@@ -1,6 +1,8 @@
+use rustc_middle::traits::query::NoSolution;
 use rustc_middle::traits::solve::{Certainty, Goal, QueryResult};
-use rustc_middle::traits::Reveal;
-use rustc_middle::ty::{self};
+use rustc_middle::traits::{ObligationCause, Reveal};
+use rustc_middle::ty;
+use rustc_middle::ty::util::NotUniqueParam;
 
 use super::{EvalCtxt, SolverMode};
 
@@ -15,22 +17,53 @@ impl<'tcx> EvalCtxt<'_, 'tcx> {
 
         match goal.param_env.reveal() {
             Reveal::UserFacing => match self.solver_mode() {
-                SolverMode::Normal => self.probe(|ecx| {
-                    // FIXME: Check that the usage is "defining" (all free params), otherwise bail.
-                    // FIXME: This should probably just check the anchor directly
+                SolverMode::Normal => {
+                    // FIXME: at some point we should call queries without defining
+                    // new opaque types but having the existing opaque type definitions.
+                    // This will require moving this below "Prefer opaques registered already".
+                    if !self.can_define_opaque_ty(opaque_ty.def_id) {
+                        return Err(NoSolution);
+                    }
+                    // FIXME: This may have issues when the substs contain aliases...
+                    match self.tcx().uses_unique_placeholders_ignoring_regions(opaque_ty.substs) {
+                        Err(NotUniqueParam::NotParam(param)) if param.is_non_region_infer() => {
+                            return self.evaluate_added_goals_and_make_canonical_response(
+                                Certainty::AMBIGUOUS,
+                            );
+                        }
+                        Err(_) => {
+                            return Err(NoSolution);
+                        }
+                        Ok(()) => {}
+                    }
+                    // Prefer opaques registered already.
+                    let matches = self.unify_existing_opaque_tys(
+                        goal.param_env,
+                        opaque_ty,
+                        expected
+                    );
+                    if !matches.is_empty() {
+                        if let Some(response) = self.try_merge_responses(&matches) {
+                            return Ok(response);
+                        } else {
+                            return self.flounder(&matches);
+                        }
+                    }
+                    // Otherwise, define a new opaque type
                     let opaque_ty = tcx.mk_opaque(opaque_ty.def_id, opaque_ty.substs);
-                    ecx.handle_opaque_ty(expected, opaque_ty, goal.param_env)?;
-                    ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-                }),
+                    self.register_opaque_ty(expected, opaque_ty, goal.param_env)?;
+                    self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+                }
                 SolverMode::Coherence => {
                     self.evaluate_added_goals_and_make_canonical_response(Certainty::AMBIGUOUS)
                 }
             },
-            Reveal::All => self.probe(|ecx| {
+            Reveal::All => {
+                // FIXME: Add an assertion that opaque type storage is empty.
                 let actual = tcx.type_of(opaque_ty.def_id).subst(tcx, opaque_ty.substs);
-                ecx.eq(goal.param_env, expected, actual)?;
-                ecx.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
-            }),
+                self.eq(goal.param_env, expected, actual)?;
+                self.evaluate_added_goals_and_make_canonical_response(Certainty::Yes)
+            }
         }
     }
 }