diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst
index 30c514f7e59..c6f17073959 100644
--- a/docs/html/user_guide.rst
+++ b/docs/html/user_guide.rst
@@ -298,6 +298,66 @@ Example build constraints file (``build-constraints.txt``):
    cython==0.29.24
 
 
+.. _`Filtering by Upload Time`:
+
+Filtering by Upload Time
+=========================
+
+The ``--uploaded-prior-to`` option allows you to filter packages by their upload time
+to an index, only considering packages that were uploaded before a specified datetime.
+This can be useful for creating reproducible builds by ensuring you only install
+packages that were available at a known point in time.
+
+.. tab:: Unix/macOS
+
+   .. code-block:: shell
+
+      python -m pip install --uploaded-prior-to=2025-03-16T00:00:00Z SomePackage
+
+.. tab:: Windows
+
+   .. code-block:: shell
+
+      py -m pip install --uploaded-prior-to=2025-03-16T00:00:00Z SomePackage
+
+The option accepts ISO 8601 datetime strings in several formats:
+
+* ``2025-03-16`` - Date in local timezone
+* ``2025-03-16 12:30:00`` - Datetime in local timezone
+* ``2025-03-16T12:30:00Z`` - Datetime in UTC
+* ``2025-03-16T12:30:00+05:00`` - Datetime in UTC offset
+
+For consistency across machines, use either UTC format (with 'Z' suffix) or UTC offset
+format (with timezone offset like '+05:00'). Local timezone formats may produce different
+results on different machines.
+
+.. note::
+
+    This option only applies to packages from indexes, not local files. Local
+    package files are allowed regardless of the ``--uploaded-prior-to`` setting.
+    e.g., ``pip install /path/to/package.whl`` or packages from
+    ``--find-links`` directories.
+
+    This option requires package indexes that provide upload-time metadata
+    (such as PyPI). If the index does not provide upload-time metadata for a
+    package file, pip will fail immediately with an error message indicating
+    that upload-time metadata is required when using ``--uploaded-prior-to``.
+
+You can combine this option with other filtering mechanisms like constraints files:
+
+.. tab:: Unix/macOS
+
+   .. code-block:: shell
+
+      python -m pip install -c constraints.txt --uploaded-prior-to=2025-03-16 SomePackage
+
+.. tab:: Windows
+
+   .. code-block:: shell
+
+      py -m pip install -c constraints.txt --uploaded-prior-to=2025-03-16 SomePackage
+
+
 .. _`Dependency Groups`:
 
 
diff --git a/news/13625.feature.rst b/news/13625.feature.rst
new file mode 100644
index 00000000000..5768cff8788
--- /dev/null
+++ b/news/13625.feature.rst
@@ -0,0 +1,2 @@
+Add ``--uploaded-prior-to`` option to only consider packages uploaded prior to
+a given datetime when the ``upload-time`` field is available from a remote index.
diff --git a/pyproject.toml b/pyproject.toml
index 813300c26de..b7089703802 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -268,7 +268,7 @@ max-complexity = 33  # default is 10
 [tool.ruff.lint.pylint]
 max-args = 15  # default is 5
 max-branches = 28  # default is 12
-max-returns = 13  # default is 6
+max-returns = 15  # default is 6
 max-statements = 134  # default is 50
 
 [tool.ruff.per-file-target-version]
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
index f28d862f279..307c5f9de2a 100644
--- a/src/pip/_internal/build_env.py
+++ b/src/pip/_internal/build_env.py
@@ -230,6 +230,8 @@ def install(
             # in the isolated build environment
             extra_environ = {"extra_environ": {"_PIP_IN_BUILD_IGNORE_CONSTRAINTS": "1"}}
 
+        if finder.uploaded_prior_to:
+            args.extend(["--uploaded-prior-to", finder.uploaded_prior_to.isoformat()])
         args.append("--")
         args.extend(requirements)
 
diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py
index b22d85da4ec..73e25e36032 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -29,6 +29,7 @@
 from pip._internal.models.format_control import FormatControl
 from pip._internal.models.index import PyPI
 from pip._internal.models.target_python import TargetPython
+from pip._internal.utils.datetime import parse_iso_datetime
 from pip._internal.utils.hashes import STRONG_HASHES
 from pip._internal.utils.misc import strtobool
 
@@ -834,6 +835,54 @@ def _handle_dependency_group(
     help="Ignore the Requires-Python information.",
 )
 
+
+def _handle_uploaded_prior_to(
+    option: Option, opt: str, value: str, parser: OptionParser
+) -> None:
+    """
+    This is an optparse.Option callback for the --uploaded-prior-to option.
+
+    Parses an ISO 8601 datetime string. If no timezone is specified in the string,
+    local timezone is used.
+
+    Note: This option only works with indexes that provide upload-time metadata
+    as specified in the simple repository API:
+    https://packaging.python.org/en/latest/specifications/simple-repository-api/
+    """
+    if value is None:
+        return None
+
+    try:
+        uploaded_prior_to = parse_iso_datetime(value)
+        # Use local timezone if no offset is given in the ISO string.
+        if uploaded_prior_to.tzinfo is None:
+            uploaded_prior_to = uploaded_prior_to.astimezone()
+        parser.values.uploaded_prior_to = uploaded_prior_to
+    except ValueError as exc:
+        msg = (
+            f"invalid --uploaded-prior-to value: {value!r}: {exc}. "
+            f"Expected an ISO 8601 datetime string, "
+            f"e.g '2023-01-01' or '2023-01-01T00:00:00Z'"
+        )
+        raise_option_error(parser, option=option, msg=msg)
+
+
+uploaded_prior_to: Callable[..., Option] = partial(
+    Option,
+    "--uploaded-prior-to",
+    dest="uploaded_prior_to",
+    metavar="datetime",
+    action="callback",
+    callback=_handle_uploaded_prior_to,
+    type="str",
+    help=(
+        "Only consider packages uploaded prior to the given date time. "
+        "Accepts ISO 8601 strings (e.g., '2023-01-01T00:00:00Z'). "
+        "Uses local timezone if none specified. Only effective when "
+        "installing from indexes that provide upload-time metadata."
+    ),
+)
+
 no_build_isolation: Callable[..., Option] = partial(
     Option,
     "--no-build-isolation",
diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py
index 640ac9fb908..44aee20e819 100644
--- a/src/pip/_internal/cli/req_command.py
+++ b/src/pip/_internal/cli/req_command.py
@@ -371,4 +371,5 @@ def _build_package_finder(
             link_collector=link_collector,
             selection_prefs=selection_prefs,
             target_python=target_python,
+            uploaded_prior_to=options.uploaded_prior_to,
         )
diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py
index 595774892af..6a4752ad710 100644
--- a/src/pip/_internal/commands/download.py
+++ b/src/pip/_internal/commands/download.py
@@ -52,6 +52,7 @@ def add_options(self) -> None:
         self.cmd_opts.add_option(cmdoptions.no_use_pep517())
         self.cmd_opts.add_option(cmdoptions.check_build_deps())
         self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
+        self.cmd_opts.add_option(cmdoptions.uploaded_prior_to())
 
         self.cmd_opts.add_option(
             "-d",
diff --git a/src/pip/_internal/commands/index.py b/src/pip/_internal/commands/index.py
index ecac99888db..b53099452e0 100644
--- a/src/pip/_internal/commands/index.py
+++ b/src/pip/_internal/commands/index.py
@@ -40,6 +40,7 @@ def add_options(self) -> None:
         cmdoptions.add_target_python_options(self.cmd_opts)
 
         self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
+        self.cmd_opts.add_option(cmdoptions.uploaded_prior_to())
         self.cmd_opts.add_option(cmdoptions.pre())
         self.cmd_opts.add_option(cmdoptions.json())
         self.cmd_opts.add_option(cmdoptions.no_binary())
@@ -103,6 +104,7 @@ def _build_package_finder(
             link_collector=link_collector,
             selection_prefs=selection_prefs,
             target_python=target_python,
+            uploaded_prior_to=options.uploaded_prior_to,
         )
 
     def get_available_package_versions(self, options: Values, args: list[Any]) -> None:
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index 8a9e914a613..97103f8651c 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -208,6 +208,7 @@ def add_options(self) -> None:
             ),
         )
 
+        self.cmd_opts.add_option(cmdoptions.uploaded_prior_to())
         self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
         self.cmd_opts.add_option(cmdoptions.no_build_isolation())
         self.cmd_opts.add_option(cmdoptions.use_pep517())
diff --git a/src/pip/_internal/commands/lock.py b/src/pip/_internal/commands/lock.py
index 71d22007f1f..ee0c464c809 100644
--- a/src/pip/_internal/commands/lock.py
+++ b/src/pip/_internal/commands/lock.py
@@ -68,6 +68,7 @@ def add_options(self) -> None:
         self.cmd_opts.add_option(cmdoptions.src())
 
         self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
+        self.cmd_opts.add_option(cmdoptions.uploaded_prior_to())
         self.cmd_opts.add_option(cmdoptions.no_build_isolation())
         self.cmd_opts.add_option(cmdoptions.use_pep517())
         self.cmd_opts.add_option(cmdoptions.no_use_pep517())
diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py
index 928019bf3c2..4cef8973061 100644
--- a/src/pip/_internal/commands/wheel.py
+++ b/src/pip/_internal/commands/wheel.py
@@ -65,6 +65,7 @@ def add_options(self) -> None:
         self.cmd_opts.add_option(cmdoptions.requirements())
         self.cmd_opts.add_option(cmdoptions.src())
         self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
+        self.cmd_opts.add_option(cmdoptions.uploaded_prior_to())
         self.cmd_opts.add_option(cmdoptions.no_deps())
         self.cmd_opts.add_option(cmdoptions.progress_bar())
 
diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py
index ae6f8962f6f..33d80822ba2 100644
--- a/src/pip/_internal/index/package_finder.py
+++ b/src/pip/_internal/index/package_finder.py
@@ -2,6 +2,7 @@
 
 from __future__ import annotations
 
+import datetime
 import enum
 import functools
 import itertools
@@ -24,10 +25,11 @@
 from pip._internal.exceptions import (
     BestVersionAlreadyInstalled,
     DistributionNotFound,
+    InstallationError,
     InvalidWheelFilename,
     UnsupportedWheel,
 )
-from pip._internal.index.collector import LinkCollector, parse_links
+from pip._internal.index.collector import IndexContent, LinkCollector, parse_links
 from pip._internal.models.candidate import InstallationCandidate
 from pip._internal.models.format_control import FormatControl
 from pip._internal.models.link import Link
@@ -111,6 +113,8 @@ class LinkType(enum.Enum):
     format_invalid = enum.auto()
     platform_mismatch = enum.auto()
     requires_python_mismatch = enum.auto()
+    upload_too_late = enum.auto()
+    upload_time_missing = enum.auto()
 
 
 class LinkEvaluator:
@@ -132,6 +136,7 @@ def __init__(
         target_python: TargetPython,
         allow_yanked: bool,
         ignore_requires_python: bool | None = None,
+        uploaded_prior_to: datetime.datetime | None = None,
     ) -> None:
         """
         :param project_name: The user supplied package name.
@@ -149,6 +154,8 @@ def __init__(
         :param ignore_requires_python: Whether to ignore incompatible
             PEP 503 "data-requires-python" values in HTML links. Defaults
             to False.
+        :param uploaded_prior_to: If set, only allow links uploaded prior to
+            the given datetime.
         """
         if ignore_requires_python is None:
             ignore_requires_python = False
@@ -158,6 +165,7 @@ def __init__(
         self._ignore_requires_python = ignore_requires_python
         self._formats = formats
         self._target_python = target_python
+        self._uploaded_prior_to = uploaded_prior_to
 
         self.project_name = project_name
 
@@ -218,6 +226,30 @@ def evaluate_link(self, link: Link) -> tuple[LinkType, str]:
 
                 version = wheel.version
 
+        # Check upload-time filter after verifying the link is a package file.
+        # Skip this check for local files, as --uploaded-prior-to only applies
+        # to packages from indexes.
+        if self._uploaded_prior_to is not None and not link.is_file:
+            if link.upload_time is None:
+                if isinstance(link.comes_from, IndexContent):
+                    index_info = f"Index {link.comes_from.url}"
+                elif link.comes_from:
+                    index_info = f"Index {link.comes_from}"
+                else:
+                    index_info = "Index"
+
+                return (
+                    LinkType.upload_time_missing,
+                    f"{index_info} does not provide upload-time metadata. "
+                    "Cannot use --uploaded-prior-to with this index.",
+                )
+            elif link.upload_time >= self._uploaded_prior_to:
+                return (
+                    LinkType.upload_too_late,
+                    f"Upload time {link.upload_time} not "
+                    f"prior to {self._uploaded_prior_to}",
+                )
+
         # This should be up by the self.ok_binary check, but see issue 2700.
         if "source" not in self._formats and ext != WHEEL_EXTENSION:
             reason = f"No sources permitted for {self.project_name}"
@@ -593,6 +625,7 @@ def __init__(
         format_control: FormatControl | None = None,
         candidate_prefs: CandidatePreferences | None = None,
         ignore_requires_python: bool | None = None,
+        uploaded_prior_to: datetime.datetime | None = None,
     ) -> None:
         """
         This constructor is primarily meant to be used by the create() class
@@ -614,6 +647,7 @@ def __init__(
         self._ignore_requires_python = ignore_requires_python
         self._link_collector = link_collector
         self._target_python = target_python
+        self._uploaded_prior_to = uploaded_prior_to
 
         self.format_control = format_control
 
@@ -637,6 +671,7 @@ def create(
         link_collector: LinkCollector,
         selection_prefs: SelectionPreferences,
         target_python: TargetPython | None = None,
+        uploaded_prior_to: datetime.datetime | None = None,
     ) -> PackageFinder:
         """Create a PackageFinder.
 
@@ -645,6 +680,8 @@ def create(
         :param target_python: The target Python interpreter to use when
             checking compatibility. If None (the default), a TargetPython
             object will be constructed from the running Python.
+        :param uploaded_prior_to: If set, only find links uploaded prior
+            to the given datetime.
         """
         if target_python is None:
             target_python = TargetPython()
@@ -661,6 +698,7 @@ def create(
             allow_yanked=selection_prefs.allow_yanked,
             format_control=selection_prefs.format_control,
             ignore_requires_python=selection_prefs.ignore_requires_python,
+            uploaded_prior_to=uploaded_prior_to,
         )
 
     @property
@@ -720,6 +758,10 @@ def prefer_binary(self) -> bool:
     def set_prefer_binary(self) -> None:
         self._candidate_prefs.prefer_binary = True
 
+    @property
+    def uploaded_prior_to(self) -> datetime.datetime | None:
+        return self._uploaded_prior_to
+
     def requires_python_skipped_reasons(self) -> list[str]:
         reasons = {
             detail
@@ -739,6 +781,7 @@ def make_link_evaluator(self, project_name: str) -> LinkEvaluator:
             target_python=self._target_python,
             allow_yanked=self._allow_yanked,
             ignore_requires_python=self._ignore_requires_python,
+            uploaded_prior_to=self._uploaded_prior_to,
         )
 
     def _sort_links(self, links: Iterable[Link]) -> list[Link]:
@@ -773,6 +816,10 @@ def get_install_candidate(
         InstallationCandidate and return it. Otherwise, return None.
         """
         result, detail = link_evaluator.evaluate_link(link)
+        if result == LinkType.upload_time_missing:
+            # Fail immediately if the index doesn't provide upload-time
+            # when --uploaded-prior-to is specified
+            raise InstallationError(detail)
         if result != LinkType.candidate:
             self._log_skipped_link(link, result, detail)
             return None
diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py
index 2e2c0f836ac..140f2cc47db 100644
--- a/src/pip/_internal/models/link.py
+++ b/src/pip/_internal/models/link.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import datetime
 import functools
 import itertools
 import logging
@@ -7,6 +8,7 @@
 import posixpath
 import re
 import urllib.parse
+import urllib.request
 from collections.abc import Mapping
 from dataclasses import dataclass
 from typing import (
@@ -15,6 +17,7 @@
     NamedTuple,
 )
 
+from pip._internal.utils.datetime import parse_iso_datetime
 from pip._internal.utils.deprecation import deprecated
 from pip._internal.utils.filetypes import WHEEL_EXTENSION
 from pip._internal.utils.hashes import Hashes
@@ -207,6 +210,7 @@ class Link:
         "requires_python",
         "yanked_reason",
         "metadata_file_data",
+        "upload_time",
         "cache_link_parsing",
         "egg_fragment",
     ]
@@ -218,6 +222,7 @@ def __init__(
         requires_python: str | None = None,
         yanked_reason: str | None = None,
         metadata_file_data: MetadataFile | None = None,
+        upload_time: datetime.datetime | None = None,
         cache_link_parsing: bool = True,
         hashes: Mapping[str, str] | None = None,
     ) -> None:
@@ -239,6 +244,8 @@ def __init__(
             no such metadata is provided. This argument, if not None, indicates
             that a separate metadata file exists, and also optionally supplies
             hashes for that file.
+        :param upload_time: upload time of the file, or None if the information
+            is not available from the server.
         :param cache_link_parsing: A flag that is used elsewhere to determine
             whether resources retrieved from this link should be cached. PyPI
             URLs should generally have this set to False, for example.
@@ -272,6 +279,7 @@ def __init__(
         self.requires_python = requires_python if requires_python else None
         self.yanked_reason = yanked_reason
         self.metadata_file_data = metadata_file_data
+        self.upload_time = upload_time
 
         self.cache_link_parsing = cache_link_parsing
         self.egg_fragment = self._egg_fragment()
@@ -300,6 +308,12 @@ def from_json(
         if metadata_info is None:
             metadata_info = file_data.get("dist-info-metadata")
 
+        upload_time: datetime.datetime | None
+        if upload_time_data := file_data.get("upload-time"):
+            upload_time = parse_iso_datetime(upload_time_data)
+        else:
+            upload_time = None
+
         # The metadata info value may be a boolean, or a dict of hashes.
         if isinstance(metadata_info, dict):
             # The file exists, and hashes have been supplied
@@ -325,6 +339,7 @@ def from_json(
             yanked_reason=yanked_reason,
             hashes=hashes,
             metadata_file_data=metadata_file_data,
+            upload_time=upload_time,
         )
 
     @classmethod
diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py
index 5999ddb3737..79904d8905c 100644
--- a/src/pip/_internal/self_outdated_check.py
+++ b/src/pip/_internal/self_outdated_check.py
@@ -23,6 +23,7 @@
 from pip._internal.models.selection_prefs import SelectionPreferences
 from pip._internal.network.session import PipSession
 from pip._internal.utils.compat import WINDOWS
+from pip._internal.utils.datetime import parse_iso_datetime
 from pip._internal.utils.entrypoints import (
     get_best_invocation_for_this_pip,
     get_best_invocation_for_this_python,
@@ -50,15 +51,6 @@ def _get_statefile_name(key: str) -> str:
     return name
 
 
-def _convert_date(isodate: str) -> datetime.datetime:
-    """Convert an ISO format string to a date.
-
-    Handles the format 2020-01-22T14:24:01Z (trailing Z)
-    which is not supported by older versions of fromisoformat.
-    """
-    return datetime.datetime.fromisoformat(isodate.replace("Z", "+00:00"))
-
-
 class SelfCheckState:
     def __init__(self, cache_dir: str) -> None:
         self._state: dict[str, Any] = {}
@@ -93,7 +85,7 @@ def get(self, current_time: datetime.datetime) -> str | None:
             return None
 
         # Determine if we need to refresh the state
-        last_check = _convert_date(self._state["last_check"])
+        last_check = parse_iso_datetime(self._state["last_check"])
         time_since_last_check = current_time - last_check
         if time_since_last_check > _WEEK:
             return None
diff --git a/src/pip/_internal/utils/datetime.py b/src/pip/_internal/utils/datetime.py
index 776e49898f7..dfab713d9f0 100644
--- a/src/pip/_internal/utils/datetime.py
+++ b/src/pip/_internal/utils/datetime.py
@@ -1,6 +1,7 @@
 """For when pip wants to check the date or time."""
 
 import datetime
+import sys
 
 
 def today_is_later_than(year: int, month: int, day: int) -> bool:
@@ -8,3 +9,16 @@ def today_is_later_than(year: int, month: int, day: int) -> bool:
     given = datetime.date(year, month, day)
 
     return today > given
+
+
+def parse_iso_datetime(isodate: str) -> datetime.datetime:
+    """Convert an ISO format string to a datetime.
+
+    Handles the format 2020-01-22T14:24:01Z (trailing Z)
+    which is not supported by older versions of fromisoformat.
+    """
+    # Python 3.11+ supports Z suffix natively in fromisoformat
+    if sys.version_info >= (3, 11):
+        return datetime.datetime.fromisoformat(isodate)
+    else:
+        return datetime.datetime.fromisoformat(isodate.replace("Z", "+00:00"))
diff --git a/tests/functional/test_uploaded_prior_to.py b/tests/functional/test_uploaded_prior_to.py
new file mode 100644
index 00000000000..8f25719f8fe
--- /dev/null
+++ b/tests/functional/test_uploaded_prior_to.py
@@ -0,0 +1,140 @@
+"""Tests for pip install --uploaded-prior-to."""
+
+from __future__ import annotations
+
+import pytest
+
+from tests.lib import PipTestEnvironment, TestData
+from tests.lib.server import (
+    file_response,
+    make_mock_server,
+    package_page,
+    server_running,
+)
+
+
+class TestUploadedPriorTo:
+    """Test --uploaded-prior-to functionality."""
+
+    def test_uploaded_prior_to_invalid_date(
+        self, script: PipTestEnvironment, data: TestData
+    ) -> None:
+        """Test that invalid date format is rejected."""
+        result = script.pip_install_local(
+            "--uploaded-prior-to=invalid-date", "simple", expect_error=True
+        )
+        assert "invalid" in result.stderr.lower() or "error" in result.stderr.lower()
+
+    def test_uploaded_prior_to_file_index_no_upload_time(
+        self, script: PipTestEnvironment, data: TestData
+    ) -> None:
+        """Test that file:// indexes are exempt from upload-time filtering."""
+        result = script.pip(
+            "install",
+            "--index-url",
+            data.index_url("simple"),
+            "--uploaded-prior-to=2100-01-01T00:00:00",
+            "simple",
+            expect_error=False,
+        )
+        assert "Successfully installed simple" in result.stdout
+
+    def test_uploaded_prior_to_http_index_no_upload_time(
+        self, script: PipTestEnvironment, data: TestData
+    ) -> None:
+        """Test that HTTP index without upload-time causes immediate error."""
+        server = make_mock_server()
+        simple_package = data.packages / "simple-1.0.tar.gz"
+        server.mock.side_effect = [
+            package_page({"simple-1.0.tar.gz": "/files/simple-1.0.tar.gz"}),
+            file_response(simple_package),
+        ]
+
+        with server_running(server):
+            result = script.pip(
+                "install",
+                "--index-url",
+                f"http://{server.host}:{server.port}",
+                "--uploaded-prior-to=2100-01-01T00:00:00",
+                "simple",
+                expect_error=True,
+            )
+
+        assert "does not provide upload-time metadata" in result.stderr
+        assert "--uploaded-prior-to" in result.stderr or "Cannot use" in result.stderr
+
+    @pytest.mark.network
+    def test_uploaded_prior_to_with_real_pypi(self, script: PipTestEnvironment) -> None:
+        """Test filtering against real PyPI with upload-time metadata."""
+        # Test with old cutoff date - should find no matching versions
+        result = script.pip(
+            "install",
+            "--dry-run",
+            "--no-deps",
+            "--uploaded-prior-to=2010-01-01T00:00:00",
+            "requests==2.0.0",
+            expect_error=True,
+        )
+        assert "Could not find a version that satisfies" in result.stderr
+
+        # Test with future cutoff date - should find the package
+        result = script.pip(
+            "install",
+            "--dry-run",
+            "--no-deps",
+            "--uploaded-prior-to=2030-01-01T00:00:00",
+            "requests==2.0.0",
+            expect_error=False,
+        )
+        assert "Would install requests-2.0.0" in result.stdout
+
+    @pytest.mark.network
+    def test_uploaded_prior_to_date_formats(self, script: PipTestEnvironment) -> None:
+        """Test various date format strings are accepted."""
+        formats = [
+            "2030-01-01",
+            "2030-01-01T00:00:00",
+            "2030-01-01T00:00:00+00:00",
+            "2030-01-01T00:00:00-05:00",
+        ]
+
+        for date_format in formats:
+            result = script.pip(
+                "install",
+                "--dry-run",
+                "--no-deps",
+                f"--uploaded-prior-to={date_format}",
+                "requests==2.0.0",
+                expect_error=False,
+            )
+            assert "Would install requests-2.0.0" in result.stdout
+
+    def test_uploaded_prior_to_allows_local_files(
+        self, script: PipTestEnvironment, data: TestData
+    ) -> None:
+        """Test that local file installs bypass upload-time filtering."""
+        simple_wheel = data.packages / "simplewheel-1.0-py2.py3-none-any.whl"
+
+        result = script.pip(
+            "install",
+            "--no-index",
+            "--uploaded-prior-to=2000-01-01T00:00:00",
+            str(simple_wheel),
+            expect_error=False,
+        )
+        assert "Successfully installed simplewheel-1.0" in result.stdout
+
+    def test_uploaded_prior_to_allows_find_links(
+        self, script: PipTestEnvironment, data: TestData
+    ) -> None:
+        """Test that --find-links bypasses upload-time filtering."""
+        result = script.pip(
+            "install",
+            "--no-index",
+            "--find-links",
+            data.find_links,
+            "--uploaded-prior-to=2000-01-01T00:00:00",
+            "simple==1.0",
+            expect_error=False,
+        )
+        assert "Successfully installed simple-1.0" in result.stdout
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index 78fe3604480..f3102db24e1 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import datetime
 import json
 import os
 import pathlib
@@ -107,6 +108,7 @@ def make_test_finder(
     allow_all_prereleases: bool = False,
     session: PipSession | None = None,
     target_python: TargetPython | None = None,
+    uploaded_prior_to: datetime.datetime | None = None,
 ) -> PackageFinder:
     """
     Create a PackageFinder for testing purposes.
@@ -125,6 +127,7 @@ def make_test_finder(
         link_collector=link_collector,
         selection_prefs=selection_prefs,
         target_python=target_python,
+        uploaded_prior_to=uploaded_prior_to,
     )
 
 
diff --git a/tests/unit/test_cmdoptions.py b/tests/unit/test_cmdoptions.py
index 9f7e01e3cf4..228485b48d7 100644
--- a/tests/unit/test_cmdoptions.py
+++ b/tests/unit/test_cmdoptions.py
@@ -1,12 +1,17 @@
 from __future__ import annotations
 
+import datetime
 import os
+from optparse import Option, OptionParser, Values
 from pathlib import Path
 from venv import EnvBuilder
 
 import pytest
 
-from pip._internal.cli.cmdoptions import _convert_python_version
+from pip._internal.cli.cmdoptions import (
+    _convert_python_version,
+    _handle_uploaded_prior_to,
+)
 from pip._internal.cli.main_parser import identify_python_interpreter
 
 
@@ -51,3 +56,122 @@ def test_identify_python_interpreter_venv(tmpdir: Path) -> None:
 
     # Passing a non-existent file returns None
     assert identify_python_interpreter(str(tmpdir / "nonexistent")) is None
+
+
+@pytest.mark.parametrize(
+    "value, expected_datetime",
+    [
+        (
+            "2023-01-01T00:00:00+00:00",
+            datetime.datetime(2023, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc),
+        ),
+        (
+            "2023-01-01T12:00:00-05:00",
+            datetime.datetime(
+                *(2023, 1, 1, 12, 0, 0),
+                tzinfo=datetime.timezone(datetime.timedelta(hours=-5)),
+            ),
+        ),
+    ],
+)
+def test_handle_uploaded_prior_to_with_timezone(
+    value: str, expected_datetime: datetime.datetime
+) -> None:
+    """Test that timezone-aware ISO 8601 date strings are parsed correctly."""
+    option = Option("--uploaded-prior-to", dest="uploaded_prior_to")
+    opt = "--uploaded-prior-to"
+    parser = OptionParser()
+    parser.values = Values()
+
+    _handle_uploaded_prior_to(option, opt, value, parser)
+
+    result = parser.values.uploaded_prior_to
+    assert isinstance(result, datetime.datetime)
+    assert result == expected_datetime
+
+
+@pytest.mark.parametrize(
+    "value, expected_date_time",
+    [
+        # Test basic ISO 8601 formats (timezone-naive, will get local timezone)
+        ("2023-01-01T00:00:00", (2023, 1, 1, 0, 0, 0)),
+        ("2023-12-31T23:59:59", (2023, 12, 31, 23, 59, 59)),
+        # Test date only (will be extended to midnight)
+        ("2023-01-01", (2023, 1, 1, 0, 0, 0)),
+    ],
+)
+def test_handle_uploaded_prior_to_naive_dates(
+    value: str, expected_date_time: tuple[int, int, int, int, int, int]
+) -> None:
+    """Test that timezone-naive ISO 8601 date strings get local timezone applied."""
+    option = Option("--uploaded-prior-to", dest="uploaded_prior_to")
+    opt = "--uploaded-prior-to"
+    parser = OptionParser()
+    parser.values = Values()
+
+    _handle_uploaded_prior_to(option, opt, value, parser)
+
+    result = parser.values.uploaded_prior_to
+    assert isinstance(result, datetime.datetime)
+
+    # Check that the date/time components match
+    assert result.timetuple()[:6] == expected_date_time
+
+    # Check that local timezone was applied (result should not be timezone-naive)
+    assert result.tzinfo is not None
+
+    # Verify it's equivalent to what .astimezone() produces on a naive datetime
+    naive_dt = datetime.datetime(*expected_date_time)
+    expected_with_local_tz = naive_dt.astimezone()
+    assert result == expected_with_local_tz
+
+
+@pytest.mark.parametrize(
+    "invalid_value",
+    [
+        "not-a-date",
+        "2023-13-01",  # Invalid month
+        "2023-01-32",  # Invalid day
+        "2023-01-01T25:00:00",  # Invalid hour
+        "",  # Empty string
+    ],
+)
+def test_handle_uploaded_prior_to_invalid_dates(invalid_value: str) -> None:
+    """Test that invalid date strings raise SystemExit via raise_option_error."""
+    option = Option("--uploaded-prior-to", dest="uploaded_prior_to")
+    opt = "--uploaded-prior-to"
+    parser = OptionParser()
+    parser.values = Values()
+
+    with pytest.raises(SystemExit):
+        _handle_uploaded_prior_to(option, opt, invalid_value, parser)
+
+
+def test_handle_uploaded_prior_to_naive() -> None:
+    """
+    Test that a naive datetime is interpreted as local time.
+    """
+    option = Option("--uploaded-prior-to", dest="uploaded_prior_to")
+    opt = "--uploaded-prior-to"
+    parser = OptionParser()
+    parser.values = Values()
+
+    # Parse a naive datetime
+    naive_input = "2023-06-15T14:30:00"
+    _handle_uploaded_prior_to(option, opt, naive_input, parser)
+    result = parser.values.uploaded_prior_to
+
+    assert result.hour == 14, (
+        f"Expected hour=14 (from input), got hour={result.hour}. "
+        "This suggests the naive datetime was incorrectly interpreted as UTC "
+        "and converted to local timezone."
+    )
+    assert result.minute == 30
+    assert result.year == 2023
+    assert result.month == 6
+    assert result.day == 15
+
+    # Verify by creating the same datetime with explicit local timezone
+    local_tz = datetime.datetime.now().astimezone().tzinfo
+    expected = datetime.datetime(2023, 6, 15, 14, 30, 0, tzinfo=local_tz)
+    assert result == expected
diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py
index b93a576f0af..74f366b9af4 100644
--- a/tests/unit/test_finder.py
+++ b/tests/unit/test_finder.py
@@ -1,3 +1,4 @@
+import datetime
 import logging
 from collections.abc import Iterable
 from unittest.mock import Mock, patch
@@ -575,3 +576,40 @@ def test_find_all_candidates_find_links_and_index(data: TestData) -> None:
     versions = finder.find_all_candidates("simple")
     # first the find-links versions then the page versions
     assert [str(v.version) for v in versions] == ["3.0", "2.0", "1.0", "1.0"]
+
+
+class TestPackageFinderUploadedPriorTo:
+    """Test PackageFinder integration with uploaded_prior_to functionality.
+
+    Only effective with indexes that provide upload-time metadata.
+    """
+
+    def test_package_finder_create_with_uploaded_prior_to(self) -> None:
+        """Test that PackageFinder.create() accepts uploaded_prior_to parameter."""
+        uploaded_prior_to = datetime.datetime(
+            2023, 6, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
+        )
+
+        finder = make_test_finder(uploaded_prior_to=uploaded_prior_to)
+
+        assert finder._uploaded_prior_to == uploaded_prior_to
+
+    def test_package_finder_make_link_evaluator_with_uploaded_prior_to(self) -> None:
+        """Test that PackageFinder creates LinkEvaluator with uploaded_prior_to."""
+        uploaded_prior_to = datetime.datetime(
+            2023, 6, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
+        )
+
+        finder = make_test_finder(uploaded_prior_to=uploaded_prior_to)
+
+        link_evaluator = finder.make_link_evaluator("test-package")
+        assert link_evaluator._uploaded_prior_to == uploaded_prior_to
+
+    def test_package_finder_uploaded_prior_to_none(self) -> None:
+        """Test that PackageFinder works correctly when uploaded_prior_to is None."""
+        finder = make_test_finder(uploaded_prior_to=None)
+
+        assert finder._uploaded_prior_to is None
+
+        link_evaluator = finder.make_link_evaluator("test-package")
+        assert link_evaluator._uploaded_prior_to is None
diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py
index e571b441f9d..da03b45259b 100644
--- a/tests/unit/test_index.py
+++ b/tests/unit/test_index.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import datetime
 import logging
 
 import pytest
@@ -365,6 +366,197 @@ def test_filter_unallowed_hashes__log_message_with_no_match(
     check_caplog(caplog, "DEBUG", expected_message)
 
 
+class TestLinkEvaluatorUploadedPriorTo:
+    """Test the uploaded_prior_to functionality in LinkEvaluator.
+
+    Only effective with indexes that provide upload-time metadata.
+    """
+
+    def make_test_link_evaluator(
+        self, uploaded_prior_to: datetime.datetime | None = None
+    ) -> LinkEvaluator:
+        """Create a LinkEvaluator for testing."""
+        target_python = TargetPython()
+        return LinkEvaluator(
+            project_name="myproject",
+            canonical_name=canonicalize_name("myproject"),
+            formats=frozenset(["source", "binary"]),
+            target_python=target_python,
+            allow_yanked=True,
+            uploaded_prior_to=uploaded_prior_to,
+        )
+
+    @pytest.mark.parametrize(
+        "upload_time, uploaded_prior_to, expected_result",
+        [
+            # Test case: upload time is before the cutoff (should be accepted)
+            (
+                datetime.datetime(2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc),
+                datetime.datetime(2023, 6, 1, 0, 0, 0, tzinfo=datetime.timezone.utc),
+                (LinkType.candidate, "1.0"),
+            ),
+            # Test case: upload time is after the cutoff (should be rejected)
+            (
+                datetime.datetime(2023, 8, 1, 12, 0, 0, tzinfo=datetime.timezone.utc),
+                datetime.datetime(2023, 6, 1, 0, 0, 0, tzinfo=datetime.timezone.utc),
+                (
+                    LinkType.upload_too_late,
+                    "Upload time 2023-08-01 12:00:00+00:00 not prior to "
+                    "2023-06-01 00:00:00+00:00",
+                ),
+            ),
+            # Test case: upload time equals the cutoff (should be rejected)
+            (
+                datetime.datetime(2023, 6, 1, 0, 0, 0, tzinfo=datetime.timezone.utc),
+                datetime.datetime(2023, 6, 1, 0, 0, 0, tzinfo=datetime.timezone.utc),
+                (
+                    LinkType.upload_too_late,
+                    "Upload time 2023-06-01 00:00:00+00:00 not prior to "
+                    "2023-06-01 00:00:00+00:00",
+                ),
+            ),
+            # Test case: no uploaded_prior_to set (should be accepted)
+            (
+                datetime.datetime(2023, 8, 1, 12, 0, 0, tzinfo=datetime.timezone.utc),
+                None,
+                (LinkType.candidate, "1.0"),
+            ),
+        ],
+    )
+    def test_evaluate_link_uploaded_prior_to(
+        self,
+        upload_time: datetime.datetime,
+        uploaded_prior_to: datetime.datetime | None,
+        expected_result: tuple[LinkType, str],
+    ) -> None:
+        """Test that links are properly filtered by upload time."""
+        evaluator = self.make_test_link_evaluator(uploaded_prior_to)
+        link = Link(
+            "https://example.com/myproject-1.0.tar.gz",
+            upload_time=upload_time,
+        )
+
+        actual = evaluator.evaluate_link(link)
+        assert actual == expected_result
+
+    def test_evaluate_link_no_upload_time(self) -> None:
+        """Test that links with no upload time cause an error when filter is set."""
+        uploaded_prior_to = datetime.datetime(
+            2023, 6, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
+        )
+        evaluator = self.make_test_link_evaluator(uploaded_prior_to)
+
+        # Link with no upload_time should be rejected when uploaded_prior_to is set
+        link = Link("https://example.com/myproject-1.0.tar.gz")
+        actual = evaluator.evaluate_link(link)
+
+        # Should be rejected because index doesn't provide upload-time
+        assert actual[0] == LinkType.upload_time_missing
+        assert "Index does not provide upload-time metadata" in actual[1]
+
+    def test_evaluate_link_no_upload_time_no_filter(self) -> None:
+        """Test that links with no upload time are accepted when no filter is set."""
+        # No uploaded_prior_to filter set
+        evaluator = self.make_test_link_evaluator(uploaded_prior_to=None)
+
+        # Link with no upload_time should be accepted when no filter is set
+        link = Link("https://example.com/myproject-1.0.tar.gz")
+        actual = evaluator.evaluate_link(link)
+
+        # Should be accepted as candidate (assuming no other issues)
+        assert actual[0] == LinkType.candidate
+        assert actual[1] == "1.0"
+
+    def test_evaluate_link_timezone_handling(self) -> None:
+        """Test that timezone-aware datetimes are handled correctly."""
+        # Set cutoff time in UTC
+        uploaded_prior_to = datetime.datetime(
+            2023, 6, 1, 12, 0, 0, tzinfo=datetime.timezone.utc
+        )
+        evaluator = self.make_test_link_evaluator(uploaded_prior_to)
+
+        # Test upload time in different timezone (earlier in UTC)
+        upload_time_est = datetime.datetime(
+            *(2023, 6, 1, 10, 0, 0),
+            tzinfo=datetime.timezone(datetime.timedelta(hours=-5)),  # EST
+        )
+        link = Link(
+            "https://example.com/myproject-1.0.tar.gz",
+            upload_time=upload_time_est,
+        )
+
+        actual = evaluator.evaluate_link(link)
+        # 10:00 EST = 15:00 UTC, which is after 12:00 UTC cutoff
+        assert actual[0] == LinkType.upload_too_late
+
+    @pytest.mark.parametrize(
+        "uploaded_prior_to",
+        [
+            datetime.datetime(2023, 6, 1, 12, 0, 0, tzinfo=datetime.timezone.utc),
+            datetime.datetime(
+                *(2023, 6, 1, 12, 0, 0),
+                tzinfo=datetime.timezone(datetime.timedelta(hours=2)),
+            ),
+        ],
+    )
+    def test_uploaded_prior_to_different_timezone_formats(
+        self, uploaded_prior_to: datetime.datetime
+    ) -> None:
+        """Test that different timezone formats for uploaded_prior_to work."""
+        evaluator = self.make_test_link_evaluator(uploaded_prior_to)
+
+        # Create a link with upload time clearly after the cutoff
+        upload_time = datetime.datetime(
+            2023, 12, 31, 23, 59, 59, tzinfo=datetime.timezone.utc
+        )
+        link = Link(
+            "https://example.com/myproject-1.0.tar.gz",
+            upload_time=upload_time,
+        )
+
+        actual = evaluator.evaluate_link(link)
+        # Should be rejected regardless of timezone format
+        assert actual[0] == LinkType.upload_too_late
+
+    def test_uploaded_prior_to_boundary_precision(self) -> None:
+        """
+        Test that --uploaded-prior-to 2025-01-01 excludes packages
+        uploaded exactly at 2025-01-01T00:00:00.
+        """
+        # --uploaded-prior-to 2025-01-01 should be strictly less than 2025-01-01
+        cutoff_date = datetime.datetime(
+            2025, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc
+        )
+        evaluator = self.make_test_link_evaluator(uploaded_prior_to=cutoff_date)
+
+        # Package uploaded exactly at 2025-01-01T00:00:00 should be rejected
+        link_at_boundary = Link(
+            "https://example.com/myproject-1.0.tar.gz",
+            upload_time=cutoff_date,
+        )
+        result_at_boundary = evaluator.evaluate_link(link_at_boundary)
+        assert result_at_boundary[0] == LinkType.upload_too_late
+        assert "not prior to" in result_at_boundary[1]
+
+        # Package uploaded 1 second before should be accepted
+        before_cutoff = cutoff_date - datetime.timedelta(seconds=1)
+        link_before = Link(
+            "https://example.com/myproject-1.0.tar.gz",
+            upload_time=before_cutoff,
+        )
+        result_before = evaluator.evaluate_link(link_before)
+        assert result_before[0] == LinkType.candidate
+
+        # Package uploaded 1 second after should be rejected
+        after_cutoff = cutoff_date + datetime.timedelta(seconds=1)
+        link_after = Link(
+            "https://example.com/myproject-1.0.tar.gz",
+            upload_time=after_cutoff,
+        )
+        result_after = evaluator.evaluate_link(link_after)
+        assert result_after[0] == LinkType.upload_too_late
+
+
 class TestCandidateEvaluator:
     @pytest.mark.parametrize(
         "allow_all_prereleases, prefer_binary",