CodeGym /Courses /Java Server /HTTP timeouts: connectTimeout and timeout

HTTP timeouts: connectTimeout and timeout

Java Server
Level 15, Lesson 2
Available

1. Timeouts as part of correctness

A remote HTTP call is not “a fast method that sometimes returns JSON.” It is a trip into the real world, where there are traffic jams, closed doors, and mysterious signs that say “break for 15 minutes” without saying when it started. Timeouts are needed not for speed, but for predictability: so your application can honestly say, “I waited long enough; beyond this point it no longer makes sense.”

If there are no timeouts, you hand control of time over to the operating system and the network. Sometimes everything will be fine. Sometimes the call will hang for a very long time because a packet got lost somewhere along the way, a router started thinking about life, and your thread is just standing there and waiting. In a console app, this looks like “the program froze.” In the backend world, it is even worse: you are tying up a thread and cannot serve other tasks.

In our ReadLater Starter, we already have commands like catalog search .... The user runs a command and expects to either get a result or quickly and clearly learn that the catalog is unavailable. Timeouts are exactly about “quickly and clearly.”

By this point, we already have two basic things: a correct URI and a clear HttpRequest. But send() still follows the rules of the network, not the rules of an ordinary Java method. The next question in the transport layer is very practical: how long are we willing to wait, and what should we do if HttpResponse never appears.

2. Connection and response: two timeouts

When we say “the service is not responding,” it sounds like one problem, but technically there are at least two different kinds of waiting there. First, the client must connect to the remote host (that is a separate stage), and then it must receive the HTTP response (that is another stage). And the timeouts for these stages are different, because the reasons for the “hang” are different.

Conceptually, the life cycle of a synchronous request looks like this:

flowchart TD
    A[There is a URI and HttpRequest] --> B[Trying to establish a connection]
    B -->|success| C[Sending the request]
    C --> D[Waiting for a response]
    D -->|success| E[Received HttpResponse]
    B -->|too long| X["connect timeout"]
    D -->|too long| Y["request timeout"]

Now let us reinforce the difference in the form of a small table (this will be your “cheat sheet on the wall”):

What we limit by time Where we set it What we measure with What usually happens when it is exceeded
Establishing a connection (connecting to the remote host)
HttpClient.newBuilder()
          .connectTimeout(...)
Duration an exception from the HttpTimeoutException family is thrown (often HttpConnectTimeoutException)
Waiting for a response to a specific request
HttpRequest.newBuilder(...)
          .timeout(...)
Duration HttpTimeoutException is thrown (this is also an IOException)

Important: both timeouts are not about “speeding up the network”, but about “giving the application the right to stop.” No magic. We are simply saying: “I am willing to wait this long; after that, I consider the call failed.”

3. connectTimeout: connection time

We set connectTimeout at the HttpClient level. The logic is simple: since the client is the “engine” that goes over the network, the rule “how long to wait for a connection” is usually shared and repeatable. Today we are not building a complex configuration system (that will come later in the course), so we will take a reasonable training value and make it explicit directly in the code.

A minimal example (note: there is no request here — this is only client configuration):

import java.net.http.HttpClient;
import java.time.Duration;

// Configure the "engine" that will go over the network
HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(2)) // Maximum time to wait for the connection to be established
        .build(); // Clients are usually created once and reused

At this point, beginners often ask: “Is 2 seconds normal?” The answer: for a training project, more than enough. In the real world, values depend on the environment and requirements, but the habit itself is more important than the number. If you set connectTimeout to 0.1 seconds, you will get many false failures. If you set it to a minute, the program will “hang” for too long. Two or three seconds usually gives a good feeling of “we tried, but not forever.”

Another small nuance that is useful to know: HttpClient is meant to be reused. That is, “create it once and then use it for requests” is a normal strategy. If you create a new HttpClient for every request, you will not “break the internet,” but you will add unnecessary noise to your code and make configuration control more difficult.

4. HttpRequest.timeout(...): waiting for a response

The request timeout (HttpRequest.Builder.timeout(...)) is set for a specific request. This is convenient: a search can be “fast,” while fetching details can be a bit more “patient,” or vice versa. What matters to us right now is the mechanism itself and how it changes the behavior of client.send(...).

An example of GET with a timeout at the request level:

import java.net.URI;
import java.net.http.HttpRequest;
import java.time.Duration;

URI uri = URI.create("https://catalog.example/search?q=java"); // Address of the remote catalog

HttpRequest request = HttpRequest.newBuilder(uri)
        .header("Accept", "application/json") // Ask for JSON in the response
        .timeout(Duration.ofSeconds(3)) // Maximum time to wait for a response to this request
        .GET() // HTTP method
        .build();

This gives good top-down readability: first the address, then the format expectations (Accept), then the time limit (timeout), then the method. The builder lets the request look like a mini document: “what I want and under what conditions.”

If you do not set timeout(...) at all, the request may wait for a very long time. Yes, sometimes everything is fast. But with network problems, you get “eternal waiting.” So if you want an adult client, timeouts are not an option; they are the default.

5. client.send(...) failures: 3 scenarios

The most important thing here is to stop thinking “error = status 500.” For the Java HTTP client, there are situations where there will be no HttpResponse at all, because the request did not finish at the network level. In that case, you get an exception. In synchronous form, client.send(...) has three “basic kinds of trouble”: timeout, network IOException, and InterruptedException.

Let us look at a minimal but honest handling template:

import java.io.IOException;
import java.net.http.HttpResponse;
import java.net.http.HttpTimeoutException;

try {
    // Synchronous call: the current thread waits for the request to finish or an exception to be thrown
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
    System.out.println("Status = " + response.statusCode()); // for example: Status = 200
} catch (HttpTimeoutException e) {
    // Timeout is a special case of IOException, so we catch it separately and first
    System.out.println("The catalog is responding too slowly"); // example user-facing message
} catch (IOException e) {
    // Other network problems go here (DNS, TLS, connection drop, etc.)
    System.out.println("Network error: " + e.getClass().getSimpleName());
} catch (InterruptedException e) {
    // The thread was asked to stop: restore the interruption flag correctly
    Thread.currentThread().interrupt();
    System.out.println("The request was interrupted");
}

Pay attention to the order of catch blocks. HttpTimeoutException is a special case of IOException. If you catch IOException first, the code will never reach HttpTimeoutException, and you will lose the ability to give the user a more specific message.

Now for InterruptedException. This is not “just another network error.” It is a signal: “the current thread was asked to stop.” In a training CLI this happens rarely, but you must always react correctly. The minimally correct reaction is to restore the interruption flag via Thread.currentThread().interrupt(); and stop normal processing. If you swallow the interruption and keep going, you can get very strange behavior later on (and, worse, it will look random).

6. IOException: preserve the meaning

IOException is a large umbrella that hides many different causes: host not found (UnknownHostException), connection refused (often when the port is wrong), network problems, TLS problems, and so on. At this point in the course, we do not need to go on an excursion through all kinds of network exceptions, but one thing is important: do not turn IOException into “well, something broke”.

Even a simple message that prints the exception type already gives you a diagnostic foothold:

} catch (IOException e) {
    // General case: DNS, connection refused, TLS, etc.
    System.out.println("Network error while calling the catalog: " + e.getClass().getSimpleName());
    System.out.println("Details: " + e.getMessage()); // The text depends on the specific cause
}

Note that this is not “logging” in the production-log sense — right now we are just in console mode, and we are honestly showing the user/developer what happened. Later on (along the course path) you will move to a proper logging stack, but the habit of preserving the meaning of the error is needed right now.

Another important principle: do not write catch (Exception e). It seems convenient, but it erases the line between timeout, network error, and interruption. And we are specifically learning to see differences. The HTTP client does not need a “universal catcher,” but a deliberate branch.

7. Timeouts without “luck”

Sometimes beginners try to “catch the timeout” against a real API and are surprised: “but it is not caught; everything is fast.” And that is a normal problem: a good service usually responds quickly, and for learning we need to see the error branch. So to demonstrate a timeout, the easiest thing is to use a clever (and a little cruel) trick: set a too small request timeout and see how the client behaves.

For example, this kind of request will almost certainly not have enough time:

import java.net.URI;
import java.net.http.HttpRequest;
import java.time.Duration;

HttpRequest request = HttpRequest.newBuilder(URI.create("https://catalog.example/search?q=java"))
        .header("Accept", "application/json") // Ask for JSON in the response
        .timeout(Duration.ofMillis(1)) // Demonstration "almost guaranteed" timeout
        .GET() // HTTP method
        .build();

When client.send(...) is called, you will very likely get HttpTimeoutException, and your catch block will run and print “The catalog is responding too slowly.” It is artificial, but very visual: you can see that the timeout branch really works and is not confused with IOException.

The same story applies to connectTimeout: sometimes with a wrong address you will get not a timeout, but, for example, UnknownHostException (domain not found) or “connection refused” (port closed). And that is useful too, because it shows: the error “service unavailable” can look different in different ways, and your code must be ready for that. Our goal is not to guess one exact error text, but to learn to distinguish categories of problems.

8. Typical mistakes when working with timeouts

Network code is the place where almost every beginner steps on the same rakes at least once. That is normal: the network is simply very unlike an ordinary method call. But it is better to see these mistakes now in a small training client than later in a large project (where they usually show up at 3 a.m. when you just wanted to sleep).

Mistake #1: “timeouts are not needed; everything is local anyway.”
Even if your service “usually responds quickly,” that is not a guarantee. Any network sometimes stalls. Any provider sometimes degrades. If you do not set timeouts, your program does not choose “how long to wait” — the OS and the circumstances choose for it. As a result, the user sees a hang and does not understand what is going on.

Mistake #2: catching IOException before HttpTimeoutException and wondering why “timeout does not work.”
HttpTimeoutException is an IOException. Therefore, the order of catch matters. If you want a separate branch for timeouts (and you almost always do), catch HttpTimeoutException first.

Mistake #3: swallowing InterruptedException and continuing execution.
Interruption is not a “minor inconvenience”; it is a shutdown protocol. Minimally correct: Thread.currentThread().interrupt(); and stop processing. If you do not do this, you can get strange problems in later code, because the thread was “asked to stop,” and you acted as if you did not hear it.

Mistake #4: setting extremely small timeouts “so it will be fast.”
A timeout is not an internet accelerator; it is a waiting limiter. A timeout that is too small turns your client into a picky visitor: “I stood at the door for 50 milliseconds and left.” In the real world, that will produce false errors. Set values so they look like a reasonable wait for a human/system, not like an attempt to beat physics.

Mistake #5: catching everything with catch(Exception e) and printing “Error.”
Such code may seem “reliable,” but it destroys meaning. You stop distinguishing timeout from a network error, and especially from interruption. As a result, the application becomes unpredictable in behavior, and diagnostics turn into reading tea leaves.

1
Task
Java Server, level 15, lesson 2
Locked
Request timeout for a slow response
Request timeout for a slow response
1
Task
Java Server, level 15, lesson 2
Locked
Network failure before receiving a response
Network failure before receiving a response
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION