diff --git a/antiope-s3.cabal b/antiope-s3.cabal
--- a/antiope-s3.cabal
+++ b/antiope-s3.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.4
 
 name:                   antiope-s3
-version:                7.4.5
+version:                7.5.0
 synopsis:               Please see the README on Github at <https://github.com/arbor/antiope#readme>
 description:            Please see the README on Github at <https://github.com/arbor/antiope#readme>.
 category:               Services
diff --git a/src/Antiope/S3.hs b/src/Antiope/S3.hs
--- a/src/Antiope/S3.hs
+++ b/src/Antiope/S3.hs
@@ -17,13 +17,15 @@
 , Region(..)
 , BucketName(..)
 , ObjectKey(..)
+, DownloadResult(..)
 , ETag(..)
 , S3Uri(..)
+, UTCTime
 ) where
 
 import Antiope.Core.Error           (handle404ToNone)
 import Antiope.S3.Internal
-import Antiope.S3.Types             (S3Uri (S3Uri, objectKey))
+import Antiope.S3.Types             (DownloadResult (..), S3Uri (S3Uri, objectKey))
 import Conduit
 import Control.Lens
 import Control.Monad
@@ -33,6 +35,7 @@
 import Data.Maybe                   (catMaybes, isJust)
 import Data.Monoid                  ((<>))
 import Data.Text                    as T (Text, pack, unpack)
+import Data.Time.Clock              (UTCTime)
 import Network.AWS                  (MonadAWS)
 import Network.AWS.Data.Body        (_streamBody)
 import Network.AWS.Data.Text        (toText)
diff --git a/src/Antiope/S3/Lazy.hs b/src/Antiope/S3/Lazy.hs
--- a/src/Antiope/S3/Lazy.hs
+++ b/src/Antiope/S3/Lazy.hs
@@ -1,27 +1,39 @@
 {-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE LambdaCase          #-}
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeApplications    #-}
 
 module Antiope.S3.Lazy
   ( unsafeDownload
   , unsafeDownloadRequest
+  , unsafeDownloadIfModifiedSince
   , download
   , downloadRequest
   , downloadFromS3Uri
+  , downloadIfModifiedSince
   , listObjectsV2
   , listS3Uris
   , dlistObjectsV2
   , dlistS3Uris
   , s3UriToListObjectsV2
+  , DownloadResult (..)
+  , S3Uri(..)
+  , AWS.BucketName
+  , AWS.ObjectKey
+  , UTCTime
   ) where
 
 import Antiope.Core.Error
 import Antiope.S3.GetObject
-import Antiope.S3.Types             (S3Uri (S3Uri), s3UriToListObjectsV2)
+import Antiope.S3.Types             (DownloadResult (..), S3Uri (S3Uri), s3UriToListObjectsV2)
 import Control.Lens
 import Control.Monad.IO.Unlift
 import Control.Monad.Trans.Resource
+import Data.Maybe                   (fromMaybe)
+import Data.Time.Clock              (UTCTime)
+import Data.Time.Clock.POSIX        (posixSecondsToUTCTime)
 import Network.AWS                  (MonadAWS)
+import Network.HTTP.Types.Status    (Status (..))
 
 import qualified Antiope.S3.Internal  as I
 import qualified Data.ByteString.Lazy as LBS
@@ -42,6 +54,33 @@
   -> AWS.ObjectKey
   -> m LBS.ByteString
 unsafeDownload bucketName objectKey = unsafeDownloadRequest (AWS.getObject bucketName objectKey)
+
+unsafeDownloadIfModifiedSince :: (MonadAWS m, MonadResource m)
+  => AWS.BucketName
+  -> AWS.ObjectKey
+  -> Maybe UTCTime
+  -> m (UTCTime, LBS.ByteString)
+unsafeDownloadIfModifiedSince bkt obj since = do
+  let req = AWS.getObject bkt obj & AWS.goIfModifiedSince .~ since
+  resp <- AWS.send req
+  -- in practice AWS will never return Nothing for the timestamp, but the API allows it
+  let modified = fromMaybe (posixSecondsToUTCTime 0) (resp ^. AWS.gorsLastModified)
+  body <- lazyByteString (resp ^. AWS.gorsBody)
+  pure (modified, body)
+
+downloadIfModifiedSince :: (MonadAWS m, MonadResource m)
+  => S3Uri
+  -> Maybe UTCTime
+  -> m (DownloadResult LBS.ByteString)
+downloadIfModifiedSince uri@(S3Uri bucketName objectKey) since =
+  handleServiceError
+    (unsafeDownloadIfModifiedSince bucketName objectKey since)
+    (\(ts, bs) -> Downloaded ts uri bs)
+    (\case
+        Status 404 _ -> Just $ NotFound uri
+        Status 304 _ -> Just $ NotModified uri
+        _            -> Nothing
+    )
 
 downloadRequest :: (MonadAWS m, MonadResource m)
   => AWS.GetObject
diff --git a/src/Antiope/S3/Strict.hs b/src/Antiope/S3/Strict.hs
--- a/src/Antiope/S3/Strict.hs
+++ b/src/Antiope/S3/Strict.hs
@@ -3,18 +3,25 @@
 module Antiope.S3.Strict
   ( unsafeDownload
   , download
+  , downloadIfModifiedSince
   , downloadFromS3Uri
-
   , unsafeDownloadMap
   , downloadMap
   , downloadMapFromS3Uri
+  , DownloadResult (..)
+  , S3Uri(..)
+  , AWS.BucketName
+  , AWS.ObjectKey
+  , UTCTime
   ) where
 
 import Antiope.Core                 ()
-import Antiope.S3.Types             (S3Uri)
+import Antiope.S3.Types             (DownloadResult, S3Uri)
+import Control.DeepSeq              (force)
 import Control.Monad                ((<$!>))
 import Control.Monad.Trans.AWS      hiding (send)
 import Control.Monad.Trans.Resource
+import Data.Time.Clock              (UTCTime)
 import Network.AWS                  (MonadAWS)
 
 import qualified Antiope.S3.Lazy      as LBS
@@ -33,6 +40,13 @@
   -> AWS.ObjectKey
   -> m (Maybe BS.ByteString)
 download bucketName objectKey = runResourceT $  (LBS.toStrict <$!>) <$!> LBS.download bucketName objectKey
+
+downloadIfModifiedSince :: (MonadAWS m, MonadUnliftIO m)
+  => S3Uri
+  -> Maybe UTCTime
+  -> m (DownloadResult BS.ByteString)
+downloadIfModifiedSince uri since =
+  runResourceT $ fmap (force . LBS.toStrict) <$!> LBS.downloadIfModifiedSince uri since
 
 downloadFromS3Uri :: (MonadAWS m, MonadUnliftIO m)
   => S3Uri
diff --git a/src/Antiope/S3/Types.hs b/src/Antiope/S3/Types.hs
--- a/src/Antiope/S3/Types.hs
+++ b/src/Antiope/S3/Types.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE DataKinds         #-}
 {-# LANGUAGE DeriveAnyClass    #-}
+{-# LANGUAGE DeriveFunctor     #-}
 {-# LANGUAGE DeriveGeneric     #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE TypeApplications  #-}
@@ -9,6 +10,7 @@
   , X.ObjectKey(..)
   , X.ETag(..)
   , S3Uri(..)
+  , DownloadResult(..)
   , readBucketName
   , readWhile
   , dirname
@@ -26,6 +28,7 @@
 import Data.Generics.Product.Any
 import Data.List
 import Data.Semigroup            ((<>))
+import Data.Time.Clock           (UTCTime)
 import GHC.Generics
 import Network.AWS.Data
 import Network.AWS.S3            (BucketName (..), ObjectKey (..))
@@ -39,6 +42,13 @@
 import qualified Network.AWS.S3                  as AWS
 import qualified Network.AWS.S3.Types            as X
 import qualified Text.ParserCombinators.ReadPrec as RP
+
+data DownloadResult a
+  = NotFound S3Uri
+  | NotModified S3Uri
+  | Downloaded UTCTime S3Uri a
+  deriving (Show, Ord, Eq, Functor)
+
 
 data S3Uri = S3Uri
   { bucket    :: BucketName
diff --git a/test/Antiope/S3/MessagesSpec.hs b/test/Antiope/S3/MessagesSpec.hs
--- a/test/Antiope/S3/MessagesSpec.hs
+++ b/test/Antiope/S3/MessagesSpec.hs
@@ -15,7 +15,7 @@
 import qualified Hedgehog.Gen   as Gen
 import qualified Hedgehog.Range as Range
 
-{-# ANN module ("HLint: Ignore Redundant do" :: String) #-}
+{-# ANN module ("HLint: ignore Redundant do" :: String) #-}
 
 genUTCTime :: MonadGen m => m UTCTime
 genUTCTime = do
diff --git a/test/Antiope/S3/TypesSpec.hs b/test/Antiope/S3/TypesSpec.hs
--- a/test/Antiope/S3/TypesSpec.hs
+++ b/test/Antiope/S3/TypesSpec.hs
@@ -6,7 +6,7 @@
 import Hedgehog
 import Test.Hspec
 
-{-# ANN module ("HLint: Ignore Redundant do" :: String) #-}
+{-# ANN module ("HLint: ignore Redundant do" :: String) #-}
 
 spec :: Spec
 spec = describe "Antiope.TypesSpec" $ do
diff --git a/test/Antiope/S3Spec.hs b/test/Antiope/S3Spec.hs
--- a/test/Antiope/S3Spec.hs
+++ b/test/Antiope/S3Spec.hs
@@ -12,7 +12,7 @@
 import qualified Antiope.S3.Lazy            as LBS
 import qualified Data.ByteString.Lazy.Char8 as LBS
 
-{-# ANN module ("HLint: Ignore Redundant do" :: String) #-}
+{-# ANN module ("HLint: ignore Redundant do" :: String) #-}
 
 spec :: Spec
 spec = describe "Antiope.S3Spec" $ do
