diff --git a/README.md b/README.md
index d4a81a2..82627f8 100644
--- a/README.md
+++ b/README.md
@@ -203,6 +203,7 @@ Config:
 - `config clopen <server> hidden [category]` -- configure where the "hidden" channels are placed.
 - `config clopen <server> owner_timeout [duration]` -- configure how long (initially) since the last message by the owner before the owner is prompted about closure, and how long until the channel is automatically closed if there was no response.
 - `config clopen <server> timeout [duration]` -- configure how long since the last message by anyone else before the owner is prompted about closure.
+- `config clopen <server> timeout_cap [duration]` -- configure how long channels can stay open for each occupying session
 - `config clopen <server> min_avail [number]` -- configure how many channels minimum should be "available". If not enough channels are available, channels may be unhidden, or new channels may be created.
 - `config clopen <server> max_avail [number]` -- configure how many channels maximum should be "available". If too many channels are available, some may be hidden.
 - `config clopen <server> max_channels [number]` -- configure the max number of channels that can be created. This number should not exceed 50, as is is impossible to place more than 50 channels in a category.
diff --git a/migrations/plugins.clopen-b25ab226ad95b4f90af72b7b29ec81ed6c6be5fa-4f94377eea1aacbc54e8179ca19edc966343fb15.sql b/migrations/plugins.clopen-b25ab226ad95b4f90af72b7b29ec81ed6c6be5fa-4f94377eea1aacbc54e8179ca19edc966343fb15.sql
new file mode 100644
index 0000000..7e262a4
--- /dev/null
+++ b/migrations/plugins.clopen-b25ab226ad95b4f90af72b7b29ec81ed6c6be5fa-4f94377eea1aacbc54e8179ca19edc966343fb15.sql
@@ -0,0 +1,4 @@
+ALTER TABLE clopen.channels ADD COLUMN max_expiry TIMESTAMP;
+ALTER TABLE clopen.guilds
+ADD COLUMN timeout_cap INTERVAL NOT NULL
+DEFAULT INTERVAL '3 DAY';
diff --git a/plugins/clopen.py b/plugins/clopen.py
index 5b3bd41..2bc7155 100644
--- a/plugins/clopen.py
+++ b/plugins/clopen.py
@@ -143,6 +143,8 @@ class GuildConfig:
     timeout: Mapped[timedelta] = mapped_column(INTERVAL, nullable=False)
     # How long initially until the channel becomes pending for closure after the owner talks
     owner_timeout: Mapped[timedelta] = mapped_column(INTERVAL, nullable=False)
+    # The maximum duration that a channel keeps open
+    timeout_cap: Mapped[timedelta] = mapped_column(INTERVAL, nullable=False)
     # Acceptable minimum number of channels in the available category at any time
     min_avail: Mapped[int] = mapped_column(BigInteger, nullable=False)
     # Acceptable maximum number of channels in the available category at any time
@@ -174,6 +176,7 @@ def __init__(
             hidden_category_id: int,
             timeout: timedelta,
             owner_timeout: timedelta,
+            timeout_cap: timedelta,
             min_avail: int,
             max_avail: int,
             max_channels: int,
@@ -215,6 +218,8 @@ class Channel:
     extension: Mapped[int] = mapped_column(BigInteger, nullable=False)
     # When to transition to the respective next state
     expiry: Mapped[Optional[datetime]] = mapped_column(TIMESTAMP)
+    # The maximum amount of time a channel can be kept open
+    max_expiry: Mapped[Optional[datetime]] = mapped_column(TIMESTAMP)
 
     guild: Mapped[GuildConfig] = relationship(GuildConfig, lazy="joined")
 
@@ -232,6 +237,7 @@ def __init__(
             prompt_id: Optional[int] = ...,
             op_id: Optional[int] = ...,
             expiry: Optional[datetime] = ...,
+            max_expiry: Optional[datetime] = ...,
         ) -> None:
             ...
 
@@ -264,18 +270,32 @@ async def scheduler_task() -> None:
         for config in configs:
             for channel in config.channels:
                 async with channel_locks[channel.id]:
-                    if channel.state == ChannelState.USED and channel.expiry is not None:
-                        if channel.expiry < datetime.utcnow():
+                    if (
+                        channel.state == ChannelState.USED
+                        and channel.expiry is not None
+                        and channel.max_expiry is not None
+                    ):
+                        if channel.max_expiry < datetime.utcnow():
+                            await timeout_cap_close_procedure(session, channel)
+                        elif channel.expiry < datetime.utcnow():
                             await make_pending(session, channel)
                         elif min_next is None or channel.expiry < min_next:
                             min_next = channel.expiry
-                    elif channel.state == ChannelState.PENDING and channel.expiry is not None:
-                        if channel.expiry < datetime.utcnow():
+                    elif (
+                        channel.state == ChannelState.PENDING
+                        and channel.expiry is not None
+                        and channel.max_expiry is not None
+                    ):
+                        if channel.max_expiry < datetime.utcnow():
+                            await timeout_cap_close_procedure(session, channel)
+                        elif channel.expiry < datetime.utcnow():
                             await close(session, channel, "Closed due to timeout")
                         elif min_next is None or channel.expiry < min_next:
                             min_next = channel.expiry
-                    elif channel.state == ChannelState.CLOSED:
-                        if channel.expiry is None or channel.expiry < datetime.utcnow():
+                    elif channel.state == ChannelState.CLOSED and channel.max_expiry is not None:
+                        if channel.max_expiry < datetime.utcnow():
+                            await make_hidden(session, channel)
+                        elif channel.expiry is None or channel.expiry < datetime.utcnow():
                             if (
                                 sum(channel.state == ChannelState.AVAILABLE for channel in config.channels)
                                 >= config.max_avail
@@ -310,6 +330,7 @@ async def init() -> None:
                 hidden_category_id=cast(int, conf.hidden_category),
                 timeout=timedelta(seconds=cast(int, conf.timeout)),
                 owner_timeout=timedelta(seconds=cast(int, conf.owner_timeout)),
+                timeout_cap=timedelta(seconds=cast(int, conf.timeout_cap)),
                 min_avail=cast(int, conf.min_avail),
                 max_avail=cast(int, conf.max_avail),
                 max_channels=cast(int, conf.max_channels),
@@ -323,6 +344,7 @@ async def init() -> None:
             session.add(guild)
             for i, id in enumerate(cast(List[int], conf.channels), start=1):
                 expiry = cast(Optional[float], conf[id, "expiry"])
+                max_expiry = cast(Optional[float], conf[id, "max_expiry"])
                 session.add(
                     Channel(
                         guild_id=guild_id,
@@ -334,6 +356,7 @@ async def init() -> None:
                         prompt_id=cast(Optional[int], conf[id, "prompt_id"]),
                         op_id=cast(Optional[int], conf[id, "op_id"]),
                         expiry=datetime.utcfromtimestamp(expiry) if expiry is not None else None,
+                        max_expiry=datetime.utcfromtimestamp(max_expiry) if max_expiry is not None else None
                     )
                 )
             await session.commit()
@@ -426,6 +449,7 @@ async def occupy(session: AsyncSession, channel: Channel, msg_id: int, author: U
     channel.op_id = msg_id
     channel.extension = 1
     channel.expiry = datetime.utcnow() + channel.guild.owner_timeout
+    channel.max_expiry = datetime.utcnow() + channel.guild.timeout_cap
     await session.commit()
     await enact_occupied(conf, chan, author, op_id=msg_id, old_op_id=old_op_id)
     scheduler_task.run_coalesced(0)
@@ -507,6 +531,15 @@ async def close(session: AsyncSession, channel: Channel, reason: str, *, reopen:
     scheduler_task.run_coalesced(0)
 
 
+async def timeout_cap_close_procedure(session: AsyncSession, channel: Channel) -> None:
+    assert isinstance(chan := client.get_channel(channel.id), TextChannel)
+    assert channel.owner_id is not None
+    close_reason = "{!m} Channel closed due to maximum timeout reached!".format(channel.owner_id)
+    # ping owner before closing
+    await chan.send("{!m}".format(channel.owner_id))
+    await close(session, channel, close_reason, reopen=False)
+
+
 async def make_available(session: AsyncSession, channel: Channel) -> None:
     logger.debug("Making {} available".format(channel.id))
     assert isinstance(chan := client.get_channel(channel.id), TextChannel)
@@ -1078,6 +1111,7 @@ async def config_new(
                 hidden_category_id=hidden_category.id,
                 timeout=timedelta(seconds=60),
                 owner_timeout=timedelta(seconds=60),
+                timeout_cap=timedelta(days=3),
                 min_avail=1,
                 max_avail=1,
                 max_channels=0,
@@ -1160,6 +1194,18 @@ async def config_owner_timeout(ctx: GuildContext, duration: Optional[DurationCon
             await ctx.send("\u2705")
 
 
+@config.command("timeout_cap")
+async def config_timeout_cap(ctx: GuildContext, duration: Optional[DurationConverter]) -> None:
+    async with sessionmaker() as session:
+        conf = await get_conf(session, ctx)
+        if duration is None:
+            await ctx.send(str(conf.timeout_cap))
+        else:
+            conf.timeout_cap = duration
+            await session.commit()
+            await ctx.send("\u2705")
+
+
 @config.command("min_avail")
 async def config_min_avail(ctx: GuildContext, number: Optional[int]) -> None:
     async with sessionmaker() as session: