What you have here are two stream pipelines.

These stream pipelines each consist of a source, several intermediate operations, and a terminal operation.

But the intermediate operations are lazy. This means that nothing happens unless a downstream operation requires an item. When it does, then the intermediate operation does all it needs to produce the required item, and then again waits until another item is requested, and so on.

The terminal operations are usually "eager". That is, they ask for all the items in the stream that are needed for them to complete.

So you should really think of the pipeline as the forEach asking the stream behind it for the next item, and that stream asks the stream behind it, and so on, all the way to the source.

With that in mind, let's see what we have with your first pipeline:

Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .limit(3)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));

So, the forEach is asking for the first item. That means the "B" peek needs an item, and asks the limit output stream for it, which means limit will need to ask the "A" peek, which goes to the source. An item is given, and goes all the way up to the forEach, and you get your first line:

A1B1C1

The forEach asks for another item, then another. And each time, the request is propagated up the stream, and performed. But when forEach asks for the fourth item, when the request gets to the limit, it knows that it has already given all the items it is allowed to give.

Thus, it is not asking the "A" peek for another item. It immediately indicates that its items are exhausted, and thus, no more actions are performed and forEach terminates.

What happens in the second pipeline?

    Stream.of(1,2,3,4,5,6,7,8,9)
    .peek(x->System.out.print("\nA"+x))
    .skip(6)
    .peek(x->System.out.print("B"+x))
    .forEach(x->System.out.print("C"+x));

Again, forEach is asking for the first item. This is propagated back. But when it gets to the skip, it knows it has to ask for 6 items from its upstream before it can pass one downstream. So it makes a request upstream from the "A" peek, consumes it without passing it downstream, makes another request, and so on. So the "A" peek gets 6 requests for an item and produces 6 prints, but these items are not passed down.

A1
A2
A3
A4
A5
A6

On the 7th request made by skip, the item is passed down to the "B" peek and from it to the forEach, so the full print is done:

A7B7C7

Then it's just like before. The skip will now, whenever it gets a request, ask for an item upstream and pass it downstream, as it "knows" it has already done its skipping job. So the rest of the prints are going through the entire pipe, until the source is exhausted.

Answer from RealSkeptic on Stack Overflow
🌐
Baeldung
baeldung.com › home › java › java streams › java stream skip() vs limit()
Java Stream skip() vs limit() | Baeldung
January 8, 2024 - But unlike skip(), which consumes the entire stream, as soon as limit() reaches the maximum number of items, it doesn’t consume any more items and simply returns the resulting stream.
🌐
Baeldung
baeldung.com › home › lesson 4: limit(), skip()
Lesson 4: limit(), skip() - Baeldung Membership
October 14, 2025 - When building pagination logic, the typical and correct order is skip() first, then limit(). Even though we won’t dive into parallel pipelines here, it’s worth noting (as pointed out by the methods’ Javadoc) that these operations can be costly for ordered parallel pipelines, because they ...
Top answer
1 of 5
106

What you have here are two stream pipelines.

These stream pipelines each consist of a source, several intermediate operations, and a terminal operation.

But the intermediate operations are lazy. This means that nothing happens unless a downstream operation requires an item. When it does, then the intermediate operation does all it needs to produce the required item, and then again waits until another item is requested, and so on.

The terminal operations are usually "eager". That is, they ask for all the items in the stream that are needed for them to complete.

So you should really think of the pipeline as the forEach asking the stream behind it for the next item, and that stream asks the stream behind it, and so on, all the way to the source.

With that in mind, let's see what we have with your first pipeline:

Stream.of(1,2,3,4,5,6,7,8,9)
        .peek(x->System.out.print("\nA"+x))
        .limit(3)
        .peek(x->System.out.print("B"+x))
        .forEach(x->System.out.print("C"+x));

So, the forEach is asking for the first item. That means the "B" peek needs an item, and asks the limit output stream for it, which means limit will need to ask the "A" peek, which goes to the source. An item is given, and goes all the way up to the forEach, and you get your first line:

A1B1C1

The forEach asks for another item, then another. And each time, the request is propagated up the stream, and performed. But when forEach asks for the fourth item, when the request gets to the limit, it knows that it has already given all the items it is allowed to give.

Thus, it is not asking the "A" peek for another item. It immediately indicates that its items are exhausted, and thus, no more actions are performed and forEach terminates.

What happens in the second pipeline?

    Stream.of(1,2,3,4,5,6,7,8,9)
    .peek(x->System.out.print("\nA"+x))
    .skip(6)
    .peek(x->System.out.print("B"+x))
    .forEach(x->System.out.print("C"+x));

Again, forEach is asking for the first item. This is propagated back. But when it gets to the skip, it knows it has to ask for 6 items from its upstream before it can pass one downstream. So it makes a request upstream from the "A" peek, consumes it without passing it downstream, makes another request, and so on. So the "A" peek gets 6 requests for an item and produces 6 prints, but these items are not passed down.

A1
A2
A3
A4
A5
A6

On the 7th request made by skip, the item is passed down to the "B" peek and from it to the forEach, so the full print is done:

A7B7C7

Then it's just like before. The skip will now, whenever it gets a request, ask for an item upstream and pass it downstream, as it "knows" it has already done its skipping job. So the rest of the prints are going through the entire pipe, until the source is exhausted.

2 of 5
13

The fluent notation of the streamed pipeline is what's causing this confusion. Think about it this way:

limit(3)

All the pipelined operations are evaluated lazily, except forEach(), which is a terminal operation, which triggers "execution of the pipeline".

When the pipeline is executed, intermediary stream definitions will not make any assumptions about what happens "before" or "after". All they're doing is take an input stream and transform it into an output stream:

Stream<Integer> s1 = Stream.of(1,2,3,4,5,6,7,8,9);
Stream<Integer> s2 = s1.peek(x->System.out.print("\nA"+x));
Stream<Integer> s3 = s2.limit(3);
Stream<Integer> s4 = s3.peek(x->System.out.print("B"+x));

s4.forEach(x->System.out.print("C"+x));
  • s1 contains 9 different Integer values.
  • s2 peeks at all values that pass it and prints them.
  • s3 passes the first 3 values to s4 and aborts the pipeline after the third value. No further values are produced by s3. This doesn't mean that no more values are in the pipeline. s2 would still produce (and print) more values, but no one requests those values and thus execution stops.
  • s4 again peeks at all values that pass it and prints them.
  • forEach consumes and prints whatever s4 passes to it.

Think about it this way. The whole stream is completely lazy. Only the terminal operation actively pulls new values from the pipeline. After it has pulled 3 values from s4 <- s3 <- s2 <- s1, s3 will no longer produce new values and it will no longer pull any values from s2 <- s1. While s1 -> s2 would still be able to produce 4-9, those values are just never pulled from the pipeline, and thus never printed by s2.

skip(6)

With skip() the same thing happens:

Stream<Integer> s1 = Stream.of(1,2,3,4,5,6,7,8,9);
Stream<Integer> s2 = s1.peek(x->System.out.print("\nA"+x));
Stream<Integer> s3 = s2.skip(6);
Stream<Integer> s4 = s3.peek(x->System.out.print("B"+x));

s4.forEach(x->System.out.print("C"+x));
  • s1 contains 9 different Integer values.
  • s2 peeks at all values that pass it and prints them.
  • s3 consumes the first 6 values, "skipping them", which means the first 6 values aren't passed to s4, only the subsequent values are.
  • s4 again peeks at all values that pass it and prints them.
  • forEach consumes and prints whatever s4 passes to it.

The important thing here is that s2 is not aware of the remaining pipeline skipping any values. s2 peeks at all values independently of what happens afterwards.

Another example:

Consider this pipeline, which is listed in this blog post

IntStream.iterate(0, i -> ( i + 1 ) % 2)
         .distinct()
         .limit(10)
         .forEach(System.out::println);

When you execute the above, the program will never halt. Why? Because:

IntStream i1 = IntStream.iterate(0, i -> ( i + 1 ) % 2);
IntStream i2 = i1.distinct();
IntStream i3 = i2.limit(10);

i3.forEach(System.out::println);

Which means:

  • i1 generates an infinite amount of alternating values: 0, 1, 0, 1, 0, 1, ...
  • i2 consumes all values that have been encountered before, passing on only "new" values, i.e. there are a total of 2 values coming out of i2.
  • i3 passes on 10 values, then stops.

This algorithm will never stop, because i3 waits for i2 to produce 8 more values after 0 and 1, but those values never appear, while i1 never stops feeding values to i2.

It doesn't matter that at some point in the pipeline, more than 10 values had been produced. All that matters is that i3 has never seen those 10 values.

To answer your question:

Is it just that "every action before skip is executed while not everyone before limit is"?

Nope. All operations before either skip() or limit() are executed. In both of your executions, you get A1 - A3. But limit() may short-circuit the pipeline, aborting value consumption once the event of interest (the limit is reached) has occurred.

🌐
DEV Community
dev.to › realnamehidden1_61 › java-8-stream-api-limit-and-skip-methods-46kl
Java 8 Stream API limit() and skip() methods - DEV Community
November 2, 2024 - limit(n): Limits the stream to the first n elements. skip(n): Skips the first n elements and processes the rest. ... import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class StreamLimitSkipExample { public ...
🌐
Oracle
docs.oracle.com › javase › 8 › docs › api › java › util › stream › Stream.html
Stream (Java Platform SE 8 )
3 weeks ago - Parameters: maxSize - the number ... IllegalArgumentException - if maxSize is negative · Stream<T> skip(long n) Returns a stream consisting of the remaining elements of this stream after discarding the first n elements of the stream....
🌐
Medium
medium.com › @AlexanderObregon › javas-stream-limit-method-explained-b1f872252828
Java’s Stream.limit() Method Explained | Medium
January 14, 2025 - By combining limit() with skip(), you can implement pagination efficiently, fetching only the data relevant to a specific page. The following example demonstrates a basic pagination mechanism: import java.util.Arrays; import java.util.List; ...
🌐
Medium
medium.com › @idiotN › java-8-stream-api-limit-and-skip-methods-c72a446849af
Java 8 Stream API limit() and skip() methods | by idiot | Medium
November 2, 2024 - limit(n): Limits the stream to the first n elements. skip(n): Skips the first n elements and processes the rest. ... import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class StreamLimitSkipExample { public ...
🌐
DEV Community
dev.to › realnamehidden1_61 › how-does-the-limit-method-differ-from-the-skip-method-in-streams-3lgg
How does the limit() method differ from the skip() method in streams? - DEV Community
December 8, 2024 - Paging or scrolling data (e.g., skipping records for pagination). ... You can combine limit() and skip() for scenarios like pagination, where you need to fetch a specific range of elements.
Find elsewhere
🌐
Medium
medium.com › javarevisited › how-to-use-java-streams-skip-and-limit-operation-235106559b96
How to Use Java Streams Skip() and Limit() Operation | by Suraj Mishra | Javarevisited | Medium
April 23, 2023 - Stream<T> skip(long n) - Returns a stream consisting of the remaining elements of this stream after discarding the first n elements of the stream. - If this stream contains fewer than n elements then an empty stream will be returned.
🌐
Apps Developer Blog
appsdeveloperblog.com › home › java › functional programming in java › streams – limit() and skip() operations
Java Stream limit() and skip() operations
August 10, 2022 - It returns a new Stream that contains only the first maxSize number of elements from the original stream. ... The skip() method returns a stream consisting of the remaining elements after discarding the first n elements. If this stream contains fewer than n elements, we’ll get an empty stream.
🌐
Medium
medium.com › geekculture › java-streams-limit-and-skip-24c9732c60a
Java Streams: Limit and Skip | Geek Culture
May 18, 2022 - Limit and Skip methods are used to change a stream size by keeping or discarding (skipping) the first elements. The Count method returns the stream size. Java 8 Stream Api
Top answer
1 of 1
3

The difference is the way the stream operations are ordered. Java Stream API. States that skip is cheap.

While skip() is generally a cheap operation on sequential stream pipelines, it can be quite expensive on ordered parallel pipelines, especially for large values of n, since skip(n) is constrained to skip not just any n elements, but the first n elements in the encounter order. Using an unordered stream source (such as generate(Supplier)) or removing the ordering constraint with BaseStream.unordered() may result in significant speedups of skip() in parallel pipelines, if the semantics of your situation permit. If consistency with encounter order is required, and you are experiencing poor performance or memory utilization with skip() in parallel pipelines, switching to sequential execution with BaseStream.sequential() may improve performance.

and

Limit: Returns a stream consisting of the elements of this stream, additionally performing the provided action on each element as elements are consumed from the resulting stream.

One benefit to using streams over sub-list is that you can apply filters and or the logic you ask about and it will likely be cheaper then making a sub-list. While the stream happens in order of the functions, some elements may be filtered out and you only have to do the stream once. While list items you may have to loop multiple times and use multiple objects to temporary hold the items; often times looping over the same unneeded function for that item.

While your question is very specific. These same principles would apply to whats happening under the hood in the list. Power of streams. Under the hood you still may have multiple objects from a stream; but the complexity is taken away from the programmer when doing complex operations on a collection of elements. Simply put it can replace many back to back for loops where we use top process elements. They really are utility. Streams can replace for loops.

🌐
Code with Mosh
forum.codewithmosh.com › java
Java Streams .skip() and .limit() errors - Java - Code with Mosh Forum
November 24, 2023 - Ultimate Java Part 3: Advance Topics → Streams → Sorting Streams video. movies.stream().skip(10).limit(15).forEach(movie -> System.out.println(movie)); movies.stream().limit(15).skip(5).forEach(movie -> System.out.print…
🌐
Boraji
boraji.com › java-8-stream-limit-and-skip-methods-example
https://boraji.com/java-8-stream-limit-and-skip-me...
April 3, 2017 - A blog for learner and developer to learn Java SE, Java EE, Spring Core, Spring MVC, Spring Boot, Hibernate ORM, Maven build tool, Eclipse IDE, Lambda Expression, IO Stream ,JFreeChart Library and more tutorials with easy and simple examples.
🌐
Java Code Geeks
examples.javacodegeeks.com › home › java development › core java
Java 8 Stream API - limit() & skip() - Examples Java Code Geeks - 2026
December 17, 2021 - The skip() method discards the first n elements of a stream. n cannot be a negative number and if it is higher than the size of a stream the method will return an empty stream · The limit() method retrieves the number of elements from the stream ...
🌐
ConcretePage
concretepage.com › java › java-8 › java-stream-skip
Java Stream skip()
Limit: [1, 2, 3, 4] Skip: [5, 6, 7, 8] Example-2: In this example we will run first skip on the source element and then limit on the stream returned by skip method.
🌐
HowToDoInJava
howtodoinjava.com › home › java 8 › java stream limit()
Java Stream limit() with Example - HowToDoInJava
March 30, 2022 - List<Integer> list = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) .skip(6) .collect(Collectors.toList()); System.out.println(newList); //[7, 8, 9, 10] Java 8 Stream limit() method can be useful in certain cases where we need to get the elements from a stream and the count of elements will be determined at runtime.
🌐
Oracle
docs.oracle.com › en › java › javase › 21 › docs › api › java.base › java › util › stream › Stream.html
Stream (Java SE 21 & JDK 21)
January 20, 2026 - If consistency with encounter order is required, and you are experiencing poor performance or memory utilization with limit() in parallel pipelines, switching to sequential execution with BaseStream.sequential() may improve performance. Parameters: maxSize - the number of elements the stream should be limited to · Returns: the new stream · Throws: IllegalArgumentException - if maxSize is negative · Stream<T> skip ·