diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,11 @@
 
 ## Unreleased
 
+## 1.0.2.4
+
+- Fix too eager creation of weak pointers, resulting in many needless allocations. This greatly reduces memory pressure and improves speed when inserting many symbols in a short amount of time! 
+- Only calculate the SipHash hash of an incoming text _once_ when inserting. (c.f. [#8](https://github.com/Qqwy/haskell-symbolize/issues/8))
+
 ## 1.0.2.3
 
 - Swaps the internal usage of lists in the global symbol table for `NonEmpty` lists, since they will never be empty. (PR [#5](https://github.com/Qqwy/haskell-symbolize/pull/5)). Thank you, @Bodigrim!
diff --git a/benchmarks/SymbolizeBench.hs b/benchmarks/SymbolizeBench.hs
new file mode 100644
--- /dev/null
+++ b/benchmarks/SymbolizeBench.hs
@@ -0,0 +1,33 @@
+{-# LANGUAGE GHC2021 #-}
+{-# LANGUAGE OverloadedLists #-}
+import Test.Tasty.Bench
+
+import Symbolize qualified
+
+import Data.Text (Text)
+import Data.Text qualified as Text
+import Data.Vector (Vector)
+import Data.Vector qualified as V
+
+mkTexts :: IO (Vector Text)
+mkTexts = do
+    let vec = Text.pack . show @Int <$> [0..3200]
+    pure vec
+
+roundtripMany :: Vector Text -> Vector Text
+roundtripMany = fmap (\val -> Symbolize.unintern $! Symbolize.intern $! val)
+
+
+main :: IO ()
+main = defaultMain
+  [ env mkTexts $ \texts ->
+    bgroup "intern/unintern roundtrip, no duplicates"
+    [ bench "10"  $ nf roundtripMany $! V.take 10 texts
+    , bench "100" $ nf roundtripMany $! V.take 100 texts
+    , bench "200" $ nf roundtripMany $! V.take 200 texts
+    , bench "400" $ nf roundtripMany $! V.take 400 texts
+    , bench "800" $ nf roundtripMany $! V.take 800 texts
+    , bench "1600" $ nf roundtripMany $! V.take 1600 texts
+    , bench "3200" $ nf roundtripMany $! V.take 3200 texts
+    ]
+  ]
diff --git a/src/Symbolize/SymbolTable.hs b/src/Symbolize/SymbolTable.hs
--- a/src/Symbolize/SymbolTable.hs
+++ b/src/Symbolize/SymbolTable.hs
@@ -79,26 +79,27 @@
 {-# INLINE insertGlobal #-}
 insertGlobal ba# = do
   GlobalSymbolTable gsymtab sipkey <- globalSymbolTable
-  let !key = calculateHash sipkey ba#
+  let !hash = calculateHash sipkey ba#
   -- SAFETY: If the table IORef contested, 
   -- this might trigger `weak` creation for the same bytestring from multiple threads
   -- at the same time.
   -- But finalization is idempotent, and only when a thread finally wins the Compare-and-Swap
   -- will its `weak` pointer be inserted (or alternatively another previously-inserted `ba` returned).
   -- So once this function returns, we can be sure we've returned a deduplicated ByteArray
-  !weak <- mkWeakSymbol ba# (removeGlobal key)
   IORef.atomicModifyIORef' gsymtab $ \table ->
-    case lookup ba# sipkey table of
+    case lookup ba# hash table of
       Just ba -> (table, ba)
       Nothing ->
-        (insert key weak table, ByteArray ba#)
+        let !weak = Accursed.accursedUnutterablePerformIO (mkWeakSymbol ba# (removeGlobal hash))
+        in (insert hash weak table, ByteArray ba#)
 
 lookupGlobal :: ByteArray# -> IO (Maybe ByteArray)
 {-# INLINE lookupGlobal #-}
 lookupGlobal ba# = do
   GlobalSymbolTable gsymtab sipkey <- globalSymbolTable
   table <- IORef.readIORef gsymtab
-  pure (lookup ba# sipkey table)
+  let hash = calculateHash sipkey ba#
+  pure (lookup ba# hash table)
 
 removeGlobal :: Hash -> IO ()
 {-# INLINE removeGlobal #-}
@@ -113,11 +114,10 @@
   let table' = IntMap.alter (Just . maybe (pure weak) (NonEmpty.cons weak)) (hashToInt key) table
    in SymbolTable table'
 
-lookup :: ByteArray# -> SipHash.SipKey -> SymbolTable -> Maybe ByteArray
+lookup :: ByteArray# -> Hash -> SymbolTable -> Maybe ByteArray
 {-# INLINE lookup #-}
-lookup ba# sipkey (SymbolTable table) = do
-  let !key = calculateHash sipkey ba#
-  weaks <- IntMap.lookup (hashToInt key) table
+lookup ba# hash (SymbolTable table) = do
+  weaks <- IntMap.lookup (hashToInt hash) table
   Foldable.find (\other -> other == ByteArray ba#) (aliveWeaks weaks)
 
 remove :: Hash -> SymbolTable -> SymbolTable
diff --git a/symbolize.cabal b/symbolize.cabal
--- a/symbolize.cabal
+++ b/symbolize.cabal
@@ -1,11 +1,11 @@
 cabal-version: 2.2
 
--- This file has been generated from package.yaml by hpack version 0.35.2.
+-- This file has been generated from package.yaml by hpack version 0.37.0.
 --
 -- see: https://github.com/sol/hpack
 
 name:           symbolize
-version:        1.0.2.3
+version:        1.0.2.4
 synopsis:       Efficient global Symbol table, with Garbage Collection.
 description:    Symbols, also known as Atoms or Interned Strings, are a common technique
                 to reduce memory usage and improve performance when using many small strings:
@@ -146,4 +146,35 @@
     , tasty-hunit
     , text >=2.0 && <2.2
     , text-short >=0.1.0 && <0.2
+  default-language: Haskell2010
+
+benchmark symbolize-bench
+  type: exitcode-stdio-1.0
+  main-is: SymbolizeBench.hs
+  other-modules:
+      Paths_symbolize
+  autogen-modules:
+      Paths_symbolize
+  hs-source-dirs:
+      benchmarks
+  default-extensions:
+      BangPatterns
+      OverloadedStrings
+      DeriveAnyClass
+      TypeApplications
+      NamedFieldPuns
+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -with-rtsopts=-A32m -fproc-alignment=64
+  build-depends:
+      base >=4.7 && <5
+    , binary >=0.8.9 && <0.9
+    , bytestring >=0.11.0 && <0.13
+    , containers >=0.6.0 && <0.8
+    , deepseq >=1.4.0 && <1.6
+    , hashable >=1.4.0 && <1.6
+    , random >=1.2 && <2
+    , symbolize
+    , tasty-bench
+    , text >=2.0 && <2.2
+    , text-short >=0.1.0 && <0.2
+    , vector
   default-language: Haskell2010
diff --git a/test/suite/SymbolizeTest.hs b/test/suite/SymbolizeTest.hs
--- a/test/suite/SymbolizeTest.hs
+++ b/test/suite/SymbolizeTest.hs
@@ -13,7 +13,7 @@
 -- import Test.Tasty.HUnit
 
 hprop_symbolTableIsIdempotent :: Property
-hprop_symbolTableIsIdempotent = withTests 1000 $ property $ do
+hprop_symbolTableIsIdempotent = withTests 2000 $ property $ do
   texts <- forAll $ Gen.list (Range.linear 0 200) (Gen.text (Range.linear 0 20) Gen.unicode)
   let !symbols = fmap Symbolize.intern texts
   annotateShow (fmap Data.Hashable.hash symbols)
@@ -22,7 +22,7 @@
   texts2 === texts
 
 hprop_concurrentAccessDoesNotCorruptTable :: Property
-hprop_concurrentAccessDoesNotCorruptTable = withTests 500 $ property $ do
+hprop_concurrentAccessDoesNotCorruptTable = withTests 2000 $ property $ do
   let numCores = 8
   texts <- forAll $ Gen.list (Range.linear 0 200) (Gen.text (Range.linear 0 20) Gen.unicode)
   results <- liftIO $ Control.Concurrent.Async.forConcurrently [(1 :: Integer) .. numCores] $ \_ -> do
