diff --git a/compiler/GHC/Core/Opt/Simplify/Env.hs b/compiler/GHC/Core/Opt/Simplify/Env.hs
index 596d9824db03763604a2211932e6674239bdf714..e72003d60b718f7f66fd7b310bb7b3ceab2e19a5 100644
--- a/compiler/GHC/Core/Opt/Simplify/Env.hs
+++ b/compiler/GHC/Core/Opt/Simplify/Env.hs
@@ -1237,9 +1237,8 @@ See also Note [Return type for join points] and Note [Join points and case-of-ca
 -}
 
 getSubst :: SimplEnv -> Subst
-getSubst (SimplEnv { seInScope = in_scope, seTvSubst = tv_env
-                      , seCvSubst = cv_env })
-  = mkSubst in_scope tv_env cv_env emptyIdSubstEnv
+getSubst (SimplEnv { seInScope = in_scope, seTvSubst = tv_env, seCvSubst = cv_env })
+  = mkTCvSubst in_scope tv_env cv_env
 
 substTy :: HasDebugCallStack => SimplEnv -> Type -> Type
 substTy env ty = Type.substTy (getSubst env) ty
diff --git a/compiler/GHC/Core/Opt/SpecConstr.hs b/compiler/GHC/Core/Opt/SpecConstr.hs
index 09460812a435af119d5dfb526b0e3586011533f6..16cca77553fec8209b0ba3758f065ad78bab10a8 100644
--- a/compiler/GHC/Core/Opt/SpecConstr.hs
+++ b/compiler/GHC/Core/Opt/SpecConstr.hs
@@ -81,8 +81,8 @@ import GHC.Builtin.Names ( specTyConKey )
 import GHC.Exts( SpecConstrAnnotation(..) )
 import GHC.Serialized   ( deserializeWithData )
 
-import Control.Monad    ( zipWithM )
-import Data.List (nubBy, sortBy, partition, dropWhileEnd, mapAccumL )
+import Control.Monad
+import Data.List ( sortBy, partition, dropWhileEnd, mapAccumL )
 import Data.Maybe( mapMaybe )
 import Data.Ord( comparing )
 import Data.Tuple
@@ -2381,19 +2381,23 @@ callsToNewPats :: ScEnv -> Id
 -- The "New" in the name means "patterns that are not already covered
 -- by an existing specialisation"
 callsToNewPats env fn spec_info@(SI { si_specs = done_specs }) bndr_occs calls
-  = do  { mb_pats <- mapM (callToPats env bndr_occs) calls
+  = do  { mb_pats <- mapM (callToPat env bndr_occs) calls
 
         ; let have_boring_call = any isNothing mb_pats
 
               good_pats :: [CallPat]
               good_pats = catMaybes mb_pats
 
+              in_scope = getSubstInScope (sc_subst env)
+
               -- Remove patterns we have already done
               new_pats = filterOut is_done good_pats
-              is_done p = any (samePat p . os_pat) done_specs
+              is_done p = any is_better done_specs
+                 where
+                   is_better done = betterPat in_scope (os_pat done) p
 
               -- Remove duplicates
-              non_dups = nubBy samePat new_pats
+              non_dups = subsumePats in_scope new_pats
 
               -- Remove ones that have too many worker variables
               small_pats = filterOut too_many_worker_args non_dups
@@ -2410,6 +2414,10 @@ callsToNewPats env fn spec_info@(SI { si_specs = done_specs }) bndr_occs calls
               (pats_were_discarded, trimmed_pats) = trim_pats env fn spec_info small_pats
 
 --        ; pprTraceM "callsToPats" (vcat [ text "calls to" <+> ppr fn <> colon <+> ppr calls
+--                                        , text "good_pats:" <+> ppr good_pats
+--                                        , text "new_pats:" <+> ppr new_pats
+--                                        , text "non_dups:" <+> ppr non_dups
+--                                        , text "small_pats:" <+> ppr small_pats
 --                                        , text "done_specs:" <+> ppr (map os_pat done_specs)
 --                                        , text "trimmed_pats:" <+> ppr trimmed_pats ])
 
@@ -2790,40 +2798,69 @@ valueIsWorkFree :: Value -> Bool
 valueIsWorkFree LambdaVal       = True
 valueIsWorkFree (ConVal _ args) = all exprIsWorkFree args
 
-samePat :: CallPat -> CallPat -> Bool
-samePat (CP { cp_qvars = vs1, cp_args = as1 })
-        (CP { cp_qvars = vs2, cp_args = as2 })
-  = all2 same as1 as2
+betterPat :: InScopeSet -> CallPat -> CallPat -> Bool
+-- pat1    f @a   (Just @a   (x::a))
+--      is better than
+-- pat2    f @Int (Just @Int (x::Int))
+-- That is, we can instantiate pat1 to get pat2
+-- See Note [Pattern duplicate elimination]
+betterPat is (CP { cp_qvars = vs1, cp_args = as1 })
+             (CP { cp_qvars = vs2, cp_args = as2 })
+  = case matchExprs ise vs1 as1 as2 of
+      Just (_, ms) -> all exprIsTrivial ms
+      Nothing      -> False
+  where
+    ise = ISE (is `extendInScopeSetList` vs2) (const noUnfolding)
+
+subsumePats :: InScopeSet -> [CallPat] -> [CallPat]
+-- Remove any patterns subsumed by others
+-- See Note [Pattern duplicate elimination]
+subsumePats is pats = foldr add [] pats
   where
-    -- If the args are the same, their strictness marks will be too so we don't compare those.
-    same (Var v1) (Var v2)
-        | v1 `elem` vs1 = v2 `elem` vs2
-        | v2 `elem` vs2 = False
-        | otherwise     = v1 == v2
-
-    same (Lit l1)    (Lit l2)    = l1==l2
-    same (App f1 a1) (App f2 a2) = same f1 f2 && same a1 a2
-
-    same (Type {}) (Type {}) = True     -- Note [Ignore type differences]
-    same (Coercion {}) (Coercion {}) = True
-    same (Tick _ e1) e2 = same e1 e2  -- Ignore casts and notes
-    same (Cast e1 _) e2 = same e1 e2
-    same e1 (Tick _ e2) = same e1 e2
-    same e1 (Cast e2 _) = same e1 e2
-
-    same e1 e2 = warnPprTrace (bad e1 || bad e2) "samePat" (ppr e1 $$ ppr e2) $
-                 False  -- Let, lambda, case should not occur
-    bad (Case {}) = True
-    bad (Let {})  = True
-    bad (Lam {})  = True
-    bad _other    = False
+    add :: CallPat -> [CallPat] -> [CallPat]
+    add ci [] = [ci]
+    add ci1 (ci2:cis) | betterPat is ci2 ci1 = ci2:cis
+                      | betterPat is ci1 ci2 = ci1:cis
+                      | otherwise             = ci2 : add ci1 cis
 
 {-
-Note [Ignore type differences]
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-We do not want to generate specialisations where the call patterns
-differ only in their type arguments!  Not only is it utterly useless,
-but it also means that (with polymorphic recursion) we can generate
-an infinite number of specialisations. Example is Data.Sequence.adjustTree,
-I think.
+Note [Pattern duplicate elimination]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Consider f :: (a,a) -> blah, and two calls
+   f @Int  (x,y)
+   f @Bool (p,q)
+
+The danger is that we'll generate two *essentially identical* specialisations,
+both for pairs, but with different types instantiating `a` (see #24229).
+
+But we'll only make a `CallPat` for an argument (a,b) if `foo` scrutinises
+that argument.  So SpecConstr should never need to specialise f's polymorphic
+type arguments.  Even with only one of these calls we should be able to
+generalise to the `CallPat`
+
+  cp_qvars = [a, r::a, s::a], cp_args = [@a (r,s)]
+
+Doing so isn't trivial, though.
+
+For now we content ourselves with a simpler plan: eliminate a call pattern
+if another pattern subsumes it; this is done by `subsumePats`.
+For example here are two patterns
+
+  cp_qvars = [a, r::a, s::a],  cp_args = [@a (r,s)]
+  cp_qvars = [x::Int, y::Int], cp_args = [@Int (x,y)]
+
+The first can be instantiated to the second, /by instantiating types only/.
+This subsumption relationship is checked by `betterPat`.  Note that if
+we have
+
+  cp_qvars = [a, r::a, s::a], cp_args = [@a (r,s)]
+  cp_qvars = [],              cp_args = [@Bool (True,False)]
+
+the first does *not* subsume the second; the second is more specific.
+
+In our initial example with `f @Int` and `f @Bool` neither subsumes the other,
+so we will get two essentially-identical specialisations. Boo.  We rely on our
+crude throttling mechanisms to stop this getting out of control -- with
+polymorphic recursion we can generate an infinite number of specialisations.
+Example is Data.Sequence.adjustTree, I think.
 -}
diff --git a/compiler/GHC/Core/Rules.hs b/compiler/GHC/Core/Rules.hs
index 2b32f8103f7846054238938ea32e325fc5a1ce11..65645923a1c2bc76f176bb497146404c117d6125 100644
--- a/compiler/GHC/Core/Rules.hs
+++ b/compiler/GHC/Core/Rules.hs
@@ -9,7 +9,7 @@
 -- The 'CoreRule' datatype itself is declared elsewhere.
 module GHC.Core.Rules (
         -- ** Looking up rules
-        lookupRule,
+        lookupRule, matchExprs,
 
         -- ** RuleBase, RuleEnv
         RuleBase, RuleEnv(..), mkRuleEnv, emptyRuleEnv,
@@ -720,15 +720,23 @@ matchN  :: InScopeEnv
 -- trailing ones, returning the result of applying the rule to a prefix
 -- of the actual arguments.
 
-matchN (ISE in_scope id_unf) rule_name tmpl_vars tmpl_es target_es rhs
+matchN ise _rule_name tmpl_vars tmpl_es target_es rhs
+  = do { (bind_wrapper, matched_es) <- matchExprs ise tmpl_vars tmpl_es target_es
+       ; return (bind_wrapper $
+                 mkLams tmpl_vars rhs `mkApps` matched_es) }
+
+matchExprs :: InScopeEnv -> [Var] -> [CoreExpr] -> [CoreExpr]
+           -> Maybe (BindWrapper, [CoreExpr])  -- 1-1 with the [Var]
+matchExprs (ISE in_scope id_unf) tmpl_vars tmpl_es target_es
   = do  { rule_subst <- match_exprs init_menv emptyRuleSubst tmpl_es target_es
         ; let (_, matched_es) = mapAccumL (lookup_tmpl rule_subst)
                                           (mkEmptySubst in_scope) $
                                 tmpl_vars `zip` tmpl_vars1
-              bind_wrapper = rs_binds rule_subst
+
+        ; let bind_wrapper = rs_binds rule_subst
                              -- Floated bindings; see Note [Matching lets]
-       ; return (bind_wrapper $
-                 mkLams tmpl_vars rhs `mkApps` matched_es) }
+
+        ; return (bind_wrapper, matched_es) }
   where
     (init_rn_env, tmpl_vars1) = mapAccumL rnBndrL (mkRnEnv2 in_scope) tmpl_vars
                   -- See Note [Cloning the template binders]
@@ -739,7 +747,7 @@ matchN (ISE in_scope id_unf) rule_name tmpl_vars tmpl_es target_es rhs
                    , rv_unf   = id_unf }
 
     lookup_tmpl :: RuleSubst -> Subst -> (InVar,OutVar) -> (Subst, CoreExpr)
-                   -- Need to return a RuleSubst solely for the benefit of mk_fake_ty
+                   -- Need to return a RuleSubst solely for the benefit of fake_ty
     lookup_tmpl (RS { rs_tv_subst = tv_subst, rs_id_subst = id_subst })
                 tcv_subst (tmpl_var, tmpl_var1)
         | isId tmpl_var1
@@ -768,7 +776,6 @@ matchN (ISE in_scope id_unf) rule_name tmpl_vars tmpl_es target_es rhs
     unbound tmpl_var
        = pprPanic "Template variable unbound in rewrite rule" $
          vcat [ text "Variable:" <+> ppr tmpl_var <+> dcolon <+> ppr (varType tmpl_var)
-              , text "Rule" <+> pprRuleName rule_name
               , text "Rule bndrs:" <+> ppr tmpl_vars
               , text "LHS args:" <+> ppr tmpl_es
               , text "Actual args:" <+> ppr target_es ]
diff --git a/compiler/GHC/Core/Subst.hs b/compiler/GHC/Core/Subst.hs
index 23c643b142a72fc5501d290334c6388661bc0a61..757a025db591afa2b339ae142e7e8f7d4d721812 100644
--- a/compiler/GHC/Core/Subst.hs
+++ b/compiler/GHC/Core/Subst.hs
@@ -19,7 +19,7 @@ module GHC.Core.Subst (
         substTickish, substDVarSet, substIdInfo,
 
         -- ** Operations on substitutions
-        emptySubst, mkEmptySubst, mkSubst, mkOpenSubst, isEmptySubst,
+        emptySubst, mkEmptySubst, mkTCvSubst, mkOpenSubst, isEmptySubst,
         extendIdSubst, extendIdSubstList, extendTCvSubst, extendTvSubstList,
         extendIdSubstWithClone,
         extendSubst, extendSubstList, extendSubstWithVar,
diff --git a/compiler/GHC/Core/TyCo/Subst.hs b/compiler/GHC/Core/TyCo/Subst.hs
index 35ac0e0ee2747ed2ba2f46ca5b1990eb4d2a564c..28748c7c3dafc3761502fbfa7a1018c16f614fe4 100644
--- a/compiler/GHC/Core/TyCo/Subst.hs
+++ b/compiler/GHC/Core/TyCo/Subst.hs
@@ -14,7 +14,7 @@ module GHC.Core.TyCo.Subst
         Subst(..), TvSubstEnv, CvSubstEnv, IdSubstEnv,
         emptyIdSubstEnv, emptyTvSubstEnv, emptyCvSubstEnv, composeTCvSubst,
         emptySubst, mkEmptySubst, isEmptyTCvSubst, isEmptySubst,
-        mkSubst, mkTvSubst, mkCvSubst, mkIdSubst,
+        mkTCvSubst, mkTvSubst, mkCvSubst, mkIdSubst,
         getTvSubstEnv, getIdSubstEnv,
         getCvSubstEnv, getSubstInScope, setInScope, getSubstRangeTyCoFVs,
         isInScope, elemSubst, notElemSubst, zapSubst,
@@ -271,8 +271,8 @@ isEmptyTCvSubst :: Subst -> Bool
 isEmptyTCvSubst (Subst _ _ tv_env cv_env)
   = isEmptyVarEnv tv_env && isEmptyVarEnv cv_env
 
-mkSubst :: InScopeSet -> TvSubstEnv -> CvSubstEnv -> IdSubstEnv -> Subst
-mkSubst in_scope tvs cvs ids = Subst in_scope ids tvs cvs
+mkTCvSubst :: InScopeSet -> TvSubstEnv -> CvSubstEnv -> Subst
+mkTCvSubst in_scope tvs cvs = Subst in_scope emptyIdSubstEnv tvs cvs
 
 mkIdSubst :: InScopeSet -> IdSubstEnv -> Subst
 mkIdSubst in_scope ids = Subst in_scope ids emptyTvSubstEnv emptyCvSubstEnv
diff --git a/compiler/GHC/Core/Type.hs b/compiler/GHC/Core/Type.hs
index 64af65205f6a449281d61b0fb40a4cde4b68acd3..b0febe808aa82a98a89a0b1e3b3f3fc50cbfab65 100644
--- a/compiler/GHC/Core/Type.hs
+++ b/compiler/GHC/Core/Type.hs
@@ -198,7 +198,7 @@ module GHC.Core.Type (
         -- ** Manipulating type substitutions
         emptyTvSubstEnv, emptySubst, mkEmptySubst,
 
-        mkSubst, zipTvSubst, mkTvSubstPrs,
+        mkTCvSubst, zipTvSubst, mkTvSubstPrs,
         zipTCvSubst,
         notElemSubst,
         getTvSubstEnv,
diff --git a/compiler/GHC/Core/Unify.hs b/compiler/GHC/Core/Unify.hs
index 44485b4038c6a572c9208613e0c9bd8bf84529f0..8dcbc09953be5e9db751e8c8e4697e3b52ff3bb9 100644
--- a/compiler/GHC/Core/Unify.hs
+++ b/compiler/GHC/Core/Unify.hs
@@ -1481,7 +1481,7 @@ getSubst :: UMEnv -> UM Subst
 getSubst env = do { tv_env <- getTvSubstEnv
                   ; cv_env <- getCvSubstEnv
                   ; let in_scope = rnInScopeSet (um_rn_env env)
-                  ; return (mkSubst in_scope tv_env cv_env emptyIdSubstEnv) }
+                  ; return (mkTCvSubst in_scope tv_env cv_env) }
 
 extendTvEnv :: TyVar -> Type -> UM ()
 extendTvEnv tv ty = UM $ \state ->
diff --git a/compiler/GHC/Tc/Utils/TcType.hs b/compiler/GHC/Tc/Utils/TcType.hs
index a55f1506480b0cf107396563c6a23e9a5e23a148..bcefd9d6d576462edfceb4081b0b51a9e49ebfb2 100644
--- a/compiler/GHC/Tc/Utils/TcType.hs
+++ b/compiler/GHC/Tc/Utils/TcType.hs
@@ -166,7 +166,7 @@ module GHC.Tc.Utils.TcType (
   extendSubstInScopeList, extendSubstInScopeSet, extendTvSubstAndInScope,
   Type.lookupTyVar, Type.extendTCvSubst, Type.substTyVarBndr,
   Type.extendTvSubst,
-  isInScope, mkSubst, mkTvSubst, zipTyEnv, zipCoEnv,
+  isInScope, mkTCvSubst, mkTvSubst, zipTyEnv, zipCoEnv,
   Type.substTy, substTys, substScaledTys, substTyWith, substTyWithCoVars,
   substTyAddInScope,
   substTyUnchecked, substTysUnchecked, substScaledTyUnchecked,
diff --git a/testsuite/tests/simplCore/should_compile/T24229a.hs b/testsuite/tests/simplCore/should_compile/T24229a.hs
new file mode 100644
index 0000000000000000000000000000000000000000..98f82b6ef161beebe79acca6c505e946f3907515
--- /dev/null
+++ b/testsuite/tests/simplCore/should_compile/T24229a.hs
@@ -0,0 +1,14 @@
+module T24229a where
+
+newtype N a = MkN a
+
+foo :: Int -> N (a,a) -> Maybe (a,a)
+foo 0 (MkN p)     = Just p
+foo n (MkN (x,y)) = foo (n-1) (MkN (y,x))
+
+-- We should generate ONE specialisation for $wfoo,
+-- and it should fire TWICE, regardless of the order
+-- of the following two definitions.
+
+wombat1 = foo 20 (MkN ("yes", "no"))
+wombat2 xs ys = foo 3 (MkN (xs, ys))
diff --git a/testsuite/tests/simplCore/should_compile/T24229a.stderr b/testsuite/tests/simplCore/should_compile/T24229a.stderr
new file mode 100644
index 0000000000000000000000000000000000000000..2328ccd920a7cf2f0dc1712ea36cf1996a10bf4d
--- /dev/null
+++ b/testsuite/tests/simplCore/should_compile/T24229a.stderr
@@ -0,0 +1,38 @@
+
+==================== Tidy Core ====================
+Result size of Tidy Core = {terms: 79, types: 106, coercions: 8, joins: 0/0}
+
+Rec {
+foo_$s$wfoo
+  = \ @a sc sc1 sc2 ->
+      case sc2 of ds {
+        __DEFAULT -> foo_$s$wfoo sc1 sc (-# ds 1#);
+        0# -> (# (sc, sc1) #)
+      }
+end Rec }
+
+foo
+  = \ @a ds ds1 ->
+      case ds of { I# ww ->
+      case ww of ds2 {
+        __DEFAULT -> case ds1 `cast` <Co:4> :: ... of { (x, y) -> case foo_$s$wfoo y x (-# ds2 1#) of { (# ww1 #) -> Just ww1 } };
+        0# -> Just (ds1 `cast` <Co:4> :: ...)
+      }
+      }
+
+wombat7 = "yes"#
+
+wombat6 = unpackCString# wombat7
+
+wombat5 = "no"#
+
+wombat4 = unpackCString# wombat5
+
+wombat1 = case foo_$s$wfoo wombat6 wombat4 20# of { (# ww #) -> Just ww }
+
+wombat8 = I# 3#
+
+wombat2 = \ @a xs ys -> case foo_$s$wfoo xs ys 3# of { (# ww #) -> Just ww }
+
+
+
diff --git a/testsuite/tests/simplCore/should_compile/T24229b.hs b/testsuite/tests/simplCore/should_compile/T24229b.hs
new file mode 100644
index 0000000000000000000000000000000000000000..df77d1839d8516069ca99cae9e5653cdb8b48e41
--- /dev/null
+++ b/testsuite/tests/simplCore/should_compile/T24229b.hs
@@ -0,0 +1,13 @@
+module T24229b where
+
+newtype N a = MkN a
+
+foo :: Int -> N (a,a) -> Maybe (a,a)
+foo 0 (MkN p)     = Just p
+foo n (MkN (x,y)) = foo (n-1) (MkN (y,x))
+
+-- We should generate ONE specialisation for $wfoo,
+-- and it should fire TWICE, regardless of the order
+-- of the following two definitions.
+
+wombat2 xs ys = foo 3 (MkN (xs, ys))
diff --git a/testsuite/tests/simplCore/should_compile/T24229b.stderr b/testsuite/tests/simplCore/should_compile/T24229b.stderr
new file mode 100644
index 0000000000000000000000000000000000000000..46b1c3280b4f25ae8e7c54bed0b1ef3bfed88745
--- /dev/null
+++ b/testsuite/tests/simplCore/should_compile/T24229b.stderr
@@ -0,0 +1,28 @@
+
+==================== Tidy Core ====================
+Result size of Tidy Core = {terms: 60, types: 83, coercions: 8, joins: 0/0}
+
+Rec {
+foo_$s$wfoo
+  = \ @a sc sc1 sc2 ->
+      case sc2 of ds {
+        __DEFAULT -> foo_$s$wfoo sc1 sc (-# ds 1#);
+        0# -> (# (sc, sc1) #)
+      }
+end Rec }
+
+foo
+  = \ @a ds ds1 ->
+      case ds of { I# ww ->
+      case ww of ds2 {
+        __DEFAULT -> case ds1 `cast` <Co:4> :: ... of { (x, y) -> case foo_$s$wfoo y x (-# ds2 1#) of { (# ww1 #) -> Just ww1 } };
+        0# -> Just (ds1 `cast` <Co:4> :: ...)
+      }
+      }
+
+wombat1 = I# 3#
+
+wombat2 = \ @a xs ys -> case foo_$s$wfoo xs ys 3# of { (# ww #) -> Just ww }
+
+
+