← Week 2: Tower Middleware Stack

Day 9: Timeout Middleware

Phase 2 · Jun 18, 2026

← Week 2: Tower Middleware Stack

Agenda (2–3 hours)

  • Read (45 min): tower::timeout source code on GitHub; read the tower-test README for testing Tower services
  • Study (45 min): Why does TimeoutLayer need to store a Duration, not an Instant? What happens to the inner future on timeout?
  • Practice (45 min): Implement TimeoutLayer from scratch — Layer, TimeoutService, TimeoutFuture
  • Challenge (30 min): Add per-request deadlines (not per-service duration) using HTTP headers or gRPC metadata
← Week 2: Tower Middleware Stack

Timeout as a Tower Layer

struct TimeoutLayer {
    duration: Duration,
}

struct TimeoutService<S> {
    inner: S,
    duration: Duration,
}

impl<S, Req> Service<Req> for TimeoutService<S>
where
    S: Service<Req>,
{
    type Response = S::Response;
    type Error = TimeoutError<S::Error>;
    type Future = TimeoutFuture<S::Future>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.inner.poll_ready(cx).map_err(TimeoutError::Service)
    }

    fn call(&mut self, req: Req) -> Self::Future {
        let deadline = Instant::now() + self.duration;
        TimeoutFuture { inner: self.inner.call(req), sleep: sleep_until(deadline) }
    }
}
← Week 2: Tower Middleware Stack

TimeoutFuture Implementation

pin_project! {
    struct TimeoutFuture<F> {
        #[pin] inner: F,
        #[pin] sleep: Sleep,
    }
}

impl<F, T, E> Future for TimeoutFuture<F>
where F: Future<Output = Result<T, E>>,
{
    type Output = Result<T, TimeoutError<E>>;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.project();
        // Check inner first
        if let Poll::Ready(result) = this.inner.poll(cx) {
            return Poll::Ready(result.map_err(TimeoutError::Service));
        }
        // Then check timer
        if this.sleep.poll(cx).is_ready() {
            return Poll::Ready(Err(TimeoutError::Elapsed));
        }
        Poll::Pending
    }
}
← Week 2: Tower Middleware Stack

Using pin_project

pin_project! (from the pin-project crate) generates safe Pin projection methods, letting you poll each field independently.

Without it, you'd need unsafe to project Pin<&mut Struct> onto Pin<&mut Field>.

Always use pin_project for futures that contain other futures as fields.

← Week 2: Tower Middleware Stack

Error Handling in Middleware

TimeoutError<E> wraps the inner error:

enum TimeoutError<E> {
    Elapsed,
    Service(E),
}

Callers must handle both cases. This is a general Tower pattern: middleware adds error variants.

tower::util::MapErr can flatten if callers want a single error type:

service.map_err(|e| match e { TimeoutError::Elapsed => MyErr::Timeout, TimeoutError::Service(e) => e.into() })
← Week 2: Tower Middleware Stack

Key Takeaways

  • Implement middleware by defining Layer<S>, Service<Req>, and a Future wrapping the inner future
  • pin_project! is the standard tool for polling struct fields that implement Future
  • Timeout middleware races the inner future against a sleep; drops the inner future on timeout
  • Error types accumulate through layers — use MapErr to normalize at the boundary

Tomorrow: retry logic — automatically retry failed requests with backoff.