diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,24 @@
 The format is based on Keep a Changelog and this project adheres to
 Semantic Versioning.
 
+## [0.1.2.0] - 2025-11-21
+### Added
+- Convenience constructors (`scheduleFromExprIANA`, `scheduleFromTextIANA`, and
+	`parseCronTextWithIANA`) so callers can bind schedules using canonical IANA
+	timezone names without touching `TZLabel`.
+- Criterion benchmark suite (`cabal bench`) that exercises sparse/dense cron
+	workloads, rate schedules, and timezone-aware helpers to catch performance
+	regressions.
+
+### Changed
+- Optimised `futureCronTimes` to use difference lists and lazily compute only
+	the necessary day candidates, removing quadratic `++` chains and improving
+	large run queries by ~30%.
+- README now uses an HTML function matrix that renders correctly on Hackage,
+	adds foldable IANA examples, and mentions the benchmark workflow.
+- Enabled `tests: True` in `cabal.project` so editors and `cabal test` pick up
+	the suite without extra flags.
+
 ## [0.1.1.1] - 2025-11-20
 ### Changed
 - Improved Haddock/README guidance for timezone-aware helpers, including a
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -19,6 +19,8 @@
 - Timezone-aware helpers: `AWS.EventBridge.Schedule` pairs expressions with an
   IANA timezone (via the `tz`/`tzdata` packages) and exposes `nextRunTimes`
   variants for UTC, local, or fully-zoned outputs.
+- Convenience constructors accept canonical IANA names (`"America/New_York"`)
+  in addition to the generated `TZLabel` constructors.
 - Extensive property-based test suite that mirrors the behaviour documented by
   AWS.
 
@@ -118,22 +120,87 @@
   `nextRunTimesZoned`) and reach for the conversion helpers when you want to
   avoid manual conversions.
 
-| Base input  | Output      | Function                           |
-|-------------|-------------|------------------------------------|
-| `UTCTime`   | `UTCTime`   | `nextRunTimesUTC`                  |
-| `LocalTime` | `UTCTime`   | `nextRunTimesUTCFromLocal`         |
-| `ZonedTime` | `UTCTime`   | `nextRunTimesUTCFromZoned`         |
-| `UTCTime`   | `LocalTime` | `nextRunTimesLocalFromUTC`         |
-| `LocalTime` | `LocalTime` | `nextRunTimesLocal`                |
-| `ZonedTime` | `LocalTime` | `nextRunTimesLocalFromZoned`       |
-| `UTCTime`   | `ZonedTime` | `nextRunTimesZonedFromUTC`         |
-| `LocalTime` | `ZonedTime` | `nextRunTimesZonedFromLocal`       |
-| `ZonedTime` | `ZonedTime` | `nextRunTimesZoned`                |
+<table>
+  <thead>
+    <tr>
+      <th>Base input</th>
+      <th>Output</th>
+      <th>Function</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr><td><code>UTCTime</code></td><td><code>UTCTime</code></td><td><code>nextRunTimesUTC</code></td></tr>
+    <tr><td><code>LocalTime</code></td><td><code>UTCTime</code></td><td><code>nextRunTimesUTCFromLocal</code></td></tr>
+    <tr><td><code>ZonedTime</code></td><td><code>UTCTime</code></td><td><code>nextRunTimesUTCFromZoned</code></td></tr>
+    <tr><td><code>UTCTime</code></td><td><code>LocalTime</code></td><td><code>nextRunTimesLocalFromUTC</code></td></tr>
+    <tr><td><code>LocalTime</code></td><td><code>LocalTime</code></td><td><code>nextRunTimesLocal</code></td></tr>
+    <tr><td><code>ZonedTime</code></td><td><code>LocalTime</code></td><td><code>nextRunTimesLocalFromZoned</code></td></tr>
+    <tr><td><code>UTCTime</code></td><td><code>ZonedTime</code></td><td><code>nextRunTimesZonedFromUTC</code></td></tr>
+    <tr><td><code>LocalTime</code></td><td><code>ZonedTime</code></td><td><code>nextRunTimesZonedFromLocal</code></td></tr>
+    <tr><td><code>ZonedTime</code></td><td><code>ZonedTime</code></td><td><code>nextRunTimesZoned</code></td></tr>
+  </tbody>
+</table>
 
 Use the conversion helpers when you already have a base timestamp in a specific
 representation and want the library to handle the translation for you (for
 example, UI-provided `LocalTime` that needs to be compared against UTC events).
 
+#### Working With IANA Names
+
+Prefer `scheduleFromText`/`scheduleFromExpr` when compiled code can depend on
+`TZLabel` constructors (they offer total coverage at compile time). When
+configurations or API payloads give you canonical timezone names, switch to the
+IANA-aware wrappers:
+
+- `scheduleFromExprIANA :: Text -> CronExprT -> Either String Schedule`
+- `scheduleFromTextIANA :: Text -> Text -> Either String Schedule`
+- `parseCronTextWithIANA :: Text -> Text -> Either String Schedule`
+
+These helpers validate the provided timezone name against the bundled tz
+database and return `Left` if it is unknown, preventing silent fallbacks.
+
+<details>
+  <summary>Example: parse config payloads with canonical names</summary>
+
+```haskell
+import AWS.EventBridge.Schedule
+import Data.Text (Text)
+import Data.Time (LocalTime)
+
+mkScheduleFromConfig :: Text -> Text -> Either String Schedule
+mkScheduleFromConfig tzName exprText =
+  scheduleFromTextIANA tzName exprText
+
+example :: Either String [LocalTime]
+example = do
+  sched <- mkScheduleFromConfig "Asia/Kolkata" "cron(0 9 * * ? *)"
+  nextRunTimesLocal sched (read "2025-11-15 09:00:00" :: LocalTime) 2
+```
+
+</details>
+
+<details>
+  <summary>Example: fall back to a default timezone</summary>
+
+```haskell
+import AWS.EventBridge.Schedule
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import Data.Time (UTCTime)
+
+resolveSchedule :: Maybe Text -> Text -> Either String Schedule
+resolveSchedule maybeName exprText = do
+  let tzName = fromMaybe "UTC" maybeName
+  scheduleFromTextIANA tzName exprText
+
+fromApi :: Either String [UTCTime]
+fromApi = do
+  sched <- resolveSchedule (Just "America/Los_Angeles") "cron(0 9 * * ? *)"
+  nextRunTimesUTC sched (read "2025-11-01 17:00:00 UTC" :: UTCTime) 1
+```
+
+</details>
+
 ### Error Reporting
 
 Parser and evaluator failures return `Left String` with human-readable error
@@ -162,8 +229,13 @@
 ```bash
 cabal build
 cabal test
+cabal bench
 cabal haddock --open
 ```
+
+`cabal bench` executes a Criterion suite that profiles cron-heavy workloads,
+rate schedules, and timezone-aware helpers so you can gauge regression risk
+when modifying the evaluator.
 
 ## Contributing
 
diff --git a/aws-eventbridge-cron.cabal b/aws-eventbridge-cron.cabal
--- a/aws-eventbridge-cron.cabal
+++ b/aws-eventbridge-cron.cabal
@@ -1,7 +1,7 @@
 cabal-version:      3.0
 
 name:               aws-eventbridge-cron
-version:            0.1.1.1
+version:            0.1.2.0
 synopsis:           AWS EventBridge cron, rate, and one-time parser with scheduler
 description:
   Parse AWS EventBridge cron, rate, and one-time expressions and compute
@@ -100,3 +100,18 @@
     , time
     , tz
     , tzdata
+
+benchmark aws-eventbridge-cron-bench
+  type: exitcode-stdio-1.0
+  hs-source-dirs:
+      bench
+  main-is: Main.hs
+  default-language: GHC2021
+  ghc-options: -O2
+  build-depends:
+      aws-eventbridge-cron
+    , base >=4.14 && <5
+    , criterion >=1.5 && <1.7
+    , text
+    , time
+    , tz
diff --git a/bench/Main.hs b/bench/Main.hs
new file mode 100644
--- /dev/null
+++ b/bench/Main.hs
@@ -0,0 +1,64 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main (main) where
+
+import AWS.EventBridge.Cron (CronExprT, nextRunTimes, parseCronText)
+import AWS.EventBridge.Schedule
+  ( Schedule
+  , nextRunTimesLocal
+  , scheduleFromText
+  )
+import Criterion.Main
+import Data.Time
+  ( LocalTime(..)
+  , UTCTime(..)
+  , fromGregorian
+  , secondsToDiffTime
+  , TimeOfDay(..)
+  )
+import Data.Time.Zones.All (TZLabel(..))
+
+main :: IO ()
+main = defaultMain
+  [ bench "cron sparse 256" $ nf (runCron cronSparseExpr) 256
+  , bench "cron dense 1024" $ nf (runCron cronDenseExpr) 1024
+  , bench "cron dense 4096" $ nf (runCron cronDenseExpr) 4096
+  , bench "rate sequential 2048" $ nf (runCron rateExpr) 2048
+  , bench "schedule local 512" $ nf (runSchedule scheduleNY) 512
+  ]
+
+runCron :: CronExprT -> Int -> [UTCTime]
+runCron expr limit =
+  forceRight "nextRunTimes" $ nextRunTimes expr cronBase limit
+
+runSchedule :: Schedule -> Int -> [LocalTime]
+runSchedule sched limit =
+  forceRight "nextRunTimesLocal" $ nextRunTimesLocal sched scheduleBase limit
+
+cronBase :: UTCTime
+cronBase = UTCTime (fromGregorian 2025 1 1) (secondsToDiffTime (5 * 3600))
+
+scheduleBase :: LocalTime
+scheduleBase = LocalTime (fromGregorian 2025 11 1) (TimeOfDay 8 30 0)
+
+cronSparseExpr :: CronExprT
+cronSparseExpr =
+  forceRight "parseCronText cronSparse" $ parseCronText "cron(0 9 ? NOV SUN 2025)"
+
+cronDenseExpr :: CronExprT
+cronDenseExpr =
+  forceRight "parseCronText cronDense" $
+    parseCronText "cron(0/5 0-12 ? JAN,MAR,SEP MON-FRI 2025-2027)"
+
+rateExpr :: CronExprT
+rateExpr = forceRight "parseCronText rate" $ parseCronText "rate(5 minutes)"
+
+scheduleNY :: Schedule
+scheduleNY =
+  forceRight "scheduleFromText" $
+    scheduleFromText America__New_York "cron(0/15 0-23 * * ? *)"
+
+forceRight :: String -> Either String a -> a
+forceRight label = either (error . mkMsg) id
+  where
+    mkMsg err = "benchmark setup failed in " <> label <> ": " <> err
diff --git a/src/AWS/EventBridge/Cron.hs b/src/AWS/EventBridge/Cron.hs
--- a/src/AWS/EventBridge/Cron.hs
+++ b/src/AWS/EventBridge/Cron.hs
@@ -28,6 +28,7 @@
 import Data.Time (UTCTime(..), Day, addUTCTime, utctDay)
 import Data.Time.Calendar (fromGregorian, toGregorian)
 import Data.Time.LocalTime (TimeOfDay(..), timeOfDayToTime)
+import Data.Monoid (Endo(..), appEndo)
 
 
 -- | EventBridge scheduling expression.
@@ -147,7 +148,7 @@
           baseDay = utctDay base
           (baseYear, baseMonth, baseDom) = toGregorian baseDay
 
-          collectYears :: [UTCTime] -> Int -> [Integer] -> Either String ([UTCTime], Int)
+          collectYears :: Endo [UTCTime] -> Int -> [Integer] -> Either String (Endo [UTCTime], Int)
           collectYears acc count [] = Right (acc, count)
           collectYears acc count _ | count >= target = Right (acc, count)
           collectYears acc count (y:ys)
@@ -156,7 +157,7 @@
                 (acc', count') <- collectMonths acc count y monthCandidates
                 collectYears acc' count' ys
 
-          collectMonths :: [UTCTime] -> Int -> Integer -> [Int] -> Either String ([UTCTime], Int)
+          collectMonths :: Endo [UTCTime] -> Int -> Integer -> [Int] -> Either String (Endo [UTCTime], Int)
           collectMonths acc count _ [] = Right (acc, count)
           collectMonths acc count _ _ | count >= target = Right (acc, count)
           collectMonths acc count year (m:ms)
@@ -166,7 +167,7 @@
                 (acc', count') <- collectDays acc count year m days
                 collectMonths acc' count' year ms
 
-          collectDays :: [UTCTime] -> Int -> Integer -> Int -> [Int] -> Either String ([UTCTime], Int)
+          collectDays :: Endo [UTCTime] -> Int -> Integer -> Int -> [Int] -> Either String (Endo [UTCTime], Int)
           collectDays acc count _ _ [] = Right (acc, count)
           collectDays acc count _ _ _ | count >= target = Right (acc, count)
           collectDays acc count year month (d:ds)
@@ -180,7 +181,7 @@
                       EQ -> dropWhile (< base) dayTimes
                       GT -> dayTimes
                     selected = take remaining filteredTimes
-                    acc' = acc ++ selected
+                    acc' = acc <> Endo (selected ++)
                     count' = count + length selected
                  in if count' >= target
                       then Right (acc', count')
@@ -194,13 +195,12 @@
             ]
 
           daysFor :: Integer -> Int -> Either String [Int]
-          daysFor year month = do
-            domDays <- evaluateDayOfMonthT year month domExpr
-            dowDays <- evaluateDayOfWeekT year month dowExpr
-            pure $ if domIsQuestion then dowDays else domDays
+          daysFor year month
+            | domIsQuestion = evaluateDayOfWeekT year month dowExpr
+            | otherwise = evaluateDayOfMonthT year month domExpr
 
-      (results, _) <- collectYears [] 0 yearCandidates
-      pure results
+      (results, _) <- collectYears mempty 0 yearCandidates
+      pure (appEndo results [])
   where
     domIsQuestion = isDomQuestion domExpr
     dowIsQuestion = isDowQuestion dowExpr
diff --git a/src/AWS/EventBridge/Schedule.hs b/src/AWS/EventBridge/Schedule.hs
--- a/src/AWS/EventBridge/Schedule.hs
+++ b/src/AWS/EventBridge/Schedule.hs
@@ -40,8 +40,11 @@
   ( -- * Schedule construction
     Schedule(..)
   , scheduleFromExpr
+  , scheduleFromExprIANA
   , scheduleFromText
+  , scheduleFromTextIANA
   , parseCronTextWithZone
+  , parseCronTextWithIANA
     -- * Primary evaluation helpers
   , nextRunTimesUTC
   , nextRunTimesLocal
@@ -63,6 +66,8 @@
   , scheduleKind
   )
 import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Text.Encoding (encodeUtf8)
 import Data.Time
   ( LocalTime(..)
   , ZonedTime(..)
@@ -77,7 +82,7 @@
   , timeZoneForUTCTime
   , utcToLocalTimeTZ
   )
-import Data.Time.Zones.All (TZLabel(..), tzByLabel)
+import Data.Time.Zones.All (TZLabel(..), fromTZName, tzByLabel)
 
 -- | Scheduling expression paired with its IANA timezone.
 --
@@ -102,6 +107,14 @@
   , scheduleZoneLabel = label
   }
 
+-- | Construct a 'Schedule' directly from an IANA location name such as
+-- @"America/New_York"@. This is a convenience wrapper over
+-- 'scheduleFromExpr' for callers that already store timezone identifiers as
+-- strings. Returns 'Left' when the name is unknown to the bundled tz database.
+scheduleFromExprIANA :: Text -> CronExprT -> Either String Schedule
+scheduleFromExprIANA tzName expr =
+  scheduleFromExpr <$> resolveTZLabel tzName <*> pure expr
+
 -- | Parse an EventBridge expression and attach a timezone in the same step.
 --
 -- >>> :{
@@ -115,10 +128,28 @@
 scheduleFromText label input =
   fmap (scheduleFromExpr label) (parseCronText input)
 
+-- | Parse an EventBridge expression and attach a timezone via its IANA name
+-- (for example @"Asia/Kolkata"@). This helper surfaces nicer ergonomics for
+-- API payloads or configuration files that keep the canonical string form.
+--
+-- >>> let base = read "2025-11-16 03:30:00 UTC" :: UTCTime
+-- >>> scheduleFromTextIANA "Asia/Kolkata" "cron(0 9 ? NOV SUN 2025)" >>= \sched -> nextRunTimesUTC sched base 1
+-- Right [2025-11-16 03:30:00 UTC]
+scheduleFromTextIANA :: Text -> Text -> Either String Schedule
+scheduleFromTextIANA tzName input =
+  resolveTZLabel tzName >>= \label -> scheduleFromText label input
+
 -- | Backwards-compatible alias for 'scheduleFromText'.
 parseCronTextWithZone :: TZLabel -> Text -> Either String Schedule
 parseCronTextWithZone = scheduleFromText
 
+-- | Alias for 'scheduleFromTextIANA'.
+--
+-- >>> parseCronTextWithIANA "America/New_York" "cron(0 9 * * ? *)" >>= \sched -> nextRunTimesLocal sched (read "2025-11-01 08:30:00" :: LocalTime) 1
+-- Right [2025-11-01 09:00:00]
+parseCronTextWithIANA :: Text -> Text -> Either String Schedule
+parseCronTextWithIANA = scheduleFromTextIANA
+
 -- | Local-time primary helper.
 --
 -- Evaluate run times in the schedule's local timezone, starting from a local
@@ -264,3 +295,9 @@
       localWall = utcToLocalTimeTZ zone utcVal
       tzInfo = timeZoneForUTCTime zone utcVal
    in ZonedTime localWall tzInfo
+
+resolveTZLabel :: Text -> Either String TZLabel
+resolveTZLabel tzName =
+  maybe (Left errMsg) Right (fromTZName (encodeUtf8 tzName))
+  where
+    errMsg = "unknown IANA timezone: " <> T.unpack tzName
diff --git a/test/AWS/EventBridge/ScheduleSpec.hs b/test/AWS/EventBridge/ScheduleSpec.hs
--- a/test/AWS/EventBridge/ScheduleSpec.hs
+++ b/test/AWS/EventBridge/ScheduleSpec.hs
@@ -71,6 +71,15 @@
             ]
       fmap (map show) (nextRunTimesZonedFromUTC sched baseUtc 2) @?= Right expected
 
+    , testCase "scheduleFromTextIANA resolves canonical names" $ do
+        sched <- either assertFailure pure (scheduleFromTextIANA "America/New_York" "cron(0 9 * * ? *)")
+        scheduleZoneLabel sched @?= America__New_York
+
+    , testCase "scheduleFromTextIANA surfaces unknown names" $ do
+        case scheduleFromTextIANA "Mars/Base" "cron(0 0 * * ? *)" of
+          Left msg -> msg @?= "unknown IANA timezone: Mars/Base"
+          Right _ -> assertFailure "expected a failure for an unknown timezone"
+
     , testCase "parseCronTextWithZone aliases scheduleFromText" $ do
       let expr = "cron(0 0 1 1 ? 2025)"
       schedA <- either assertFailure pure (parseCronTextWithZone Asia__Kolkata expr)
